// external dependencies import stringify from 'json-stringify-safe'; import hashIt from 'hash-it'; import {parse} from 'pathington'; import {get, has, merge, remove, set} from 'unchanged'; // classes import CrioObject from './CrioObject'; // constants import {ARRAY_FALLBACK_PROTOTYPE_METHODS, ARRAY_UNSCOPABLES} from './constants'; // is import {isArray, isCrio, isEqual, isUndefined} from './is'; // utils import { createIterator, find, getCrioedObject, getEntries, getRelativeValue, getValues, thaw } from './utils'; let hasAppliedPrototype = false; class CrioArray extends Array { constructor(array) { super(isArray(array) ? array.length : array || 0); if (!hasAppliedPrototype) { applyPrototype(); hasAppliedPrototype = true; } if (isCrio(array)) { return array.toArray(); } return isArray(array) ? array.reduce((crioArray, item, index) => { crioArray[index] = getCrioedObject(item); return crioArray; }, this) : this; } get hashCode() { return hashIt(this, true); } /** * @function clear * @memberof CrioArray *folder * @description * get a new empty array * * @returns {CrioArray} the empty array */ clear() { return new CrioArray(); } /** * @function compact * @memberof CrioArray * * @description * get a new array with values from the original array that are truthy * * @returns {CrioArray} the array with only truthy values */ compact() { return this.filter((item) => { return !!item; }); } /** * @function copyWithin * * @description * move values around within the array * * @param {number} targetIndex target to copy * @param {number} [startIndex=0] index to start copying to * @param {number} [endIndex=this.length] index to stop copying to * @returns {CrioArray} array with target copied in appropriate spots */ copyWithin(targetIndex, startIndex = 0, endIndex = this.length) { const clone = [...this]; const length = this.length >>> 0; let to = getRelativeValue(targetIndex >> 0, length), from = getRelativeValue(startIndex >> 0, length); const final = getRelativeValue(endIndex >> 0, length); let count = Math.min(final - from, length - to), direction = 1; if (from < to && to < from + count) { direction = -1; from += count - 1; to += count - 1; } while (count > 0) { if (from in clone) { clone[to] = clone[from]; } else { delete clone[to]; } from += direction; to += direction; count--; } return new this.constructor(clone); } /** * @function delete * @memberof CrioArray * * @description * delete the value in the array at key, either shallow or deep * * @param {Array<number|string>|number} key the key to delete * @returns {CrioArray} the array with the key deleted */ delete(key) { return remove(key, this); } /** * @function difference * @memberof CrioArray * * @description * find the values in this that do not exist in any of the arrays passed * * @param {...Array<Array<*>>} arrays arrays to get the difference of * @returns {CrioArray} array of items matching diffference criteria */ difference(...arrays) { if (!arrays.length) { return this; } let indexOfItem; return arrays.reduce((differenceArray, array) => { if (isArray(array)) { array.forEach((item) => { indexOfItem = differenceArray.indexOf(item); if (~indexOfItem) { differenceArray = differenceArray.splice(indexOfItem, 1); } }); } return differenceArray; }, this); } /** * @function entries * @memberof CrioArray * * @description * get the pairs of [key, value] in the crio * * @returns {CrioArray} [key, value] pairs */ entries() { return getEntries(this); } /** * @function equals * @memberof CrioArray * * @description * does the object passed equal the crio * * @param {*} object object to compare against the instance * @returns {boolean} is the object equivalent in value */ equals(object) { return isEqual(this, object); } /** * @function fill * * @description * fill the array at certain indices with the value passed * * @param {*} value the value to fill the indices with * @param {number} [startIndex=0] the starting index to fill * @param {number} [endIndex=this.length] the ending index to fill * @returns {CrioArray} array with values filled appropriately */ fill(value, startIndex = 0, endIndex = this.length) { const from = startIndex < 0 ? this.length + startIndex : startIndex; const to = endIndex < 0 ? this.length + endIndex : endIndex; const crioedValue = getCrioedObject(value); return this.map((item, index) => { return index >= from && index < to ? crioedValue : item; }); } /** * @function findLast * @memberof CrioArray * * @description * find an item in the crio if it exists, starting from the end and iteratng to the start * * @param {function} fn function to test for finding the item * @returns {*} found item or undefined */ findLast(fn) { return find(this, fn, false, true); } /** * @function findLastIndex * @memberof CrioArray * * @description * find the matching index based on truthy return from fn starting from end * * @param {function} fn function to use for test in iteration * @returns {number} index of match, or -1 */ findLastIndex(fn) { return find(this, fn, true, true); } /** * @function first * @memberof CrioArray * * @description * take the first n number of items in the array * * @param {number} [size=1] size of elements to take from beginning of array * @returns {CrioArray} */ first(size = 1) { return this.slice(0, size); } /** * @function forEach * @memberof CrioArray * * @description * iterate over the array executing fn * * @param {function} fn the function to execute * @returns {CrioArray} the original array */ forEach(fn) { Array.prototype.forEach.call(this, fn); return this; } /** * @function get * @memberof CrioArray * * @description * get the item at key passed, either shallow or deeply nested * * @param {Array<number|string>|number} key key to retrieve * @returns {*} item found at key */ get(key) { return get(key, this); } /** * @function has * @memberof CrioArray * * @description * does the crio have the key passed, either shallow or deeply nested * * @param {Array<number|string>|number} key key to test * @returns {boolean} does the crio have the key */ has(key) { return has(key, this); } /** * @function intersection * @memberof CrioArray * * @description * find the values in that exist in this and each of the arrays passed * * @param {...Array<Array>} arrays to find the intersecting values of * @returns {CrioArray} array of values that exist in all arrays */ intersection(...arrays) { if (!arrays.length) { return this; } let indices = [], indexOfItem; const reducedArrays = [this, ...arrays].reduce((items, array) => { if (isArray(array)) { array.forEach((item) => { indexOfItem = items.indexOf(item); if (~indexOfItem) { return indices[indexOfItem]++; } indices[items.length] = 1; items.push(item); }); } return items; }, []); const newLength = arrays.length + 1; return new CrioArray( reducedArrays.filter((itemIgnored, index) => { return indices[index] === newLength; }) ); } /** * @function isArray * @memberof CrioArray * * @description * is the crio an array * * @returns {boolean} is the crio an array */ isArray() { return true; } /** * @function isObject * @memberof CrioArray * * @description * is the crio an object * * @returns {boolean} is the crio an object */ isObject() { return false; } /** * @function join * @memberof CrioArray * * @description * join the values in the crio as a string, combined with separator * * @param {string} [separator] character(s) to place between strings in combination * @returns {string} parameters joined by separator in string */ join(separator) { return this.thaw().join(separator); } /** * @function keys * @memberof CrioArray * * @description * get the keys of the crio * * @returns {CrioArray<number>} keys in the crio */ keys() { return new CrioArray(Object.keys(this).map(Number)); } /** * @function last * @memberof CrioArray * * @description * take the last n number of items in the array * * @param {number} [size=1] size of elements to take from end of array * @returns {CrioArray} */ last(size = 1) { return this.slice(this.length - size); } /** * @function map * @memberof CrioArray * * @description * map over the array returning the mapped items * * @param {function} fn the function to map * @returns {CrioArray} the mapped array */ map(fn) { return Array.prototype.map.call(this, (item, index) => { return getCrioedObject(fn(item, index, this)); }); } /** * @function merge * @memberof CrioArray * * @description * merge arrays with the original array * * @param {Array<number|string>|number|null} key the key to merge into * @param {...Array<CrioArray>} objects objects to merge with the crio * @returns {CrioArray} merged array */ merge(key, ...objects) { return objects.reduce((mergedObject, object) => { return merge(key, getCrioedObject(object), mergedObject); }, this); } /** * @function mutate * @memberof CrioArray * * @description * work with the array in a mutated way and return the crioed result of that call * * @param {function} fn function to apply to crio * @returns {*} crioed value resulting from the call */ mutate(fn) { return getCrioedObject(fn(this.thaw(), this)); } /** * @function pluck * @memberof CrioArray * * @description * get the values in each object in the collection at key, either shallow or deeply nested * * @param {string} key key to find value of in collection object * @returns {CrioArray} array of plucked values */ pluck(key) { const parsedKey = parse(key); const arrayToPluck = get(parsedKey.slice(0, parsedKey.length - 1), this); const finalKey = parsedKey.slice(-1); return arrayToPluck.map((item) => { return get(finalKey, item); }); } /** * @function pop * @memberof CrioArray * * @description * get crio based on current crio with last item removed * * @returns {CrioArray} array with the last value removed */ pop() { return this.slice(0, this.length - 1); } /** * @function push * @memberof CrioArray * * @description * push one to many items to the current crio * * @param {...Array<*>} items the items to add to the array * @returns {CrioArray} array with the values added */ push(...items) { return items.length ? this.concat(items) : this; } /** * @function reduce * @memberof CrioObject * * @description * reduce the crio down to a single value, starting with initial value * * @param {function} fn the function to iterate with * @param {*} initialValue the initial value of the reduction * @returns {*} the reduced value */ reduce(fn, initialValue) { return getCrioedObject( Array.prototype.reduce.call( this, (value, item, index) => { return fn(value, item, index, this); }, initialValue ) ); } /** * @function reduceRight * @memberof CrioObject * * @description * reduce the crio down to a single value, starting with initial value, starting from the end of the array * and iterating to the start * * @param {function} fn the function to iterate with * @param {*} initialValue the initial value of the reduction * @returns {*} the reduced value */ reduceRight(fn, initialValue) { return getCrioedObject( Array.prototype.reduceRight.call( this, (value, item, index) => { return fn(value, item, index, this); }, initialValue ) ); } /** * @function reverse * @memberof CrioArray * * @description * get the same values, but in reverse order * * @returns {CrioArray} array with the items reversed in order */ reverse() { return new CrioArray([...this].reverse()); } /** * @function set * @memberof CrioArray * * @description * set the value at the key passed * * @param {Array<number|string>|number} key key to assign value to * @param {*} value value to assign * @returns {CrioArray} array with value set at key */ set(key, value) { return set(key, getCrioedObject(value), this); } /** * @function shift * @memberof CrioArray * * @description * get crio based on current crio with first item removed * * @returns {CrioArray} array with the first item removed */ shift() { return this.slice(1); } /** * @function sort * @memberof CrioArray * * @description * sort the collection by the fn passed * * @param {function} fn the function to sort based on * @returns {CrioArray} array with the items sorted */ sort(fn) { const clone = [...this]; clone.sort(fn); return new CrioArray(clone); } /** * @function splice * @memberof CrioArray * * @description * splice the values into or out of the array * * @param {number} [start=0] starting index to splice * @param {number} [deleteCount=1] length from starting index to removes * @param {...Array<*>} items items to insert after delete is complete * @returns {CrioArray} array with the value spliced in / out */ splice(...args) { const clone = [...this]; clone.splice(...args); return new CrioArray(clone); } /** * @function thaw * @memberof CrioArray * * @description * create a plain JS version of the array * * @returns {Array<*>} plain JS version of the array */ thaw() { return thaw(this); } /** * @function toArray * @memberof CrioArray * * @description * convert the array to an array * * @returns {CrioArray} the array */ toArray() { return this; } /** * @function toLocaleString * @memberof CrioArray * * @description * convert the array to stringified form * * @param {function} [serializer] the serialization method to use * @param {number} [indent] the number of spaces to indent the values * @returns {string} stringified array */ toLocaleString(serializer, indent) { return this.toString(serializer, indent); } /** * @function toObject * @memberof CrioArray * * @description * convert the crio to an object if it isn't already * * @returns {CrioObject} new object from the array */ toObject() { return this.reduce((object, item, index) => { object[index] = item; return object; }, new CrioObject({})); } /** * @function toString * @memberof CrioArray * * @description * convert the array to stringified form * * @param {function} [serializer] the serialization method to use * @param {number} [indent] the number of spaces to indent the values * @returns {string} stringified array */ toString(serializer, indent) { return stringify(this, serializer, indent); } /** * @function unique * @memberof CrioArray * * @description * return the current CrioArray with the duplicate values removed * * @returns {CrioArray} new crio instance */ unique() { let hashArray = [], newArray = [], hasHashCode = false, hashCode, storeValue; return this.filter((item) => { hashCode = item ? item.hashCode : undefined; hasHashCode = !isUndefined(hashCode); storeValue = !~newArray.indexOf(item) && (!hasHashCode || !~hashArray.indexOf(hashCode)); if (storeValue) { newArray.push(item); if (hasHashCode) { hashArray.push(hashCode); } } return storeValue; }); } /** * @function unshift * @memberof CrioArray * * @description * add items passed to the beginning of the crio array * * @param {...Array<*>} items items to prepend to the array * @returns {CrioArray} array with the items prepended */ unshift(...items) { return items.length ? new CrioArray([...items, ...this]) : this; } /** * @function valueOf * @memberof CrioArray * * @description * get the array value * * @returns {CrioArray} the array */ valueOf() { return this; } /** * @function values * @memberof CrioArray * * @description * get the values of the array as an array * * @returns {CrioArray} values in the array */ values() { return getValues(this); } /** * @function xor * @memberof CrioArray * * @description * find the values that are the symmetric difference of this and the arrays passed * * @param {Array<Array>} arrays arrays to find symmetric values in * @returns {CrioArray} array of the symmetric differences of all the arrays */ xor(...arrays) { if (!arrays.length) { return this; } let indicesToRemove = [], indexOfItem; const reducedItems = [this, ...arrays].reduce((items, array) => { if (isArray(array)) { array.forEach((item) => { indexOfItem = items.indexOf(item); if (~indexOfItem) { indicesToRemove.push(indexOfItem); } else { items = items.push(item); } }); } return items; }, new CrioArray([])); return reducedItems.filter((itemIgnored, index) => { return !~indicesToRemove.indexOf(index); }); } } export function applyPrototype() { Object.keys(ARRAY_FALLBACK_PROTOTYPE_METHODS).forEach((key) => { if (typeof Array.prototype[key] !== 'function') { CrioArray.prototype[key] = function(...args) { return ARRAY_FALLBACK_PROTOTYPE_METHODS[key](this, ...args); }; } }); if (typeof Symbol === 'function') { if (Symbol.species) { Object.defineProperty(CrioArray, Symbol.species, { configurable: false, enumerable: false, get() { return CrioArray; } }); } if (Symbol.iterator) { Object.defineProperty(CrioArray.prototype, Symbol.iterator, { configurable: false, enumerable: false, value: createIterator(), writable: false }); } if (Symbol.unscopables) { Object.defineProperty(CrioArray.prototype, Symbol.unscopables, { configurable: false, enumerable: false, value: ARRAY_UNSCOPABLES, writable: false }); } } } export default CrioArray;