UNPKG

sorted-array-type

Version:

Array with sorted insertion and optimized search methods.

899 lines 34.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SortedArray = exports.SortedArrayDefaultComparatorFunction = exports.SortedArrayDefaultEqualityFunction = void 0; /** See {@link SortedArray.DefaultEqualityFunction}. */ exports.SortedArrayDefaultEqualityFunction = ((a, b) => (a === b || (a !== a && b !== b))); /** See {@link SortedArray.DefaultComparatorFunction}. */ exports.SortedArrayDefaultComparatorFunction = ((a, b) => (a < b ? -1 : (a > b ? +1 : 0))); /** * Check if a value is iterable. * @private */ function isIterable(value) { return !!(value && typeof (value) === "object" && (typeof (value[Symbol.iterator]) === "function")); } /** * Check if a value satisfies the ArrayLike interface. * @private */ function isArrayLike(value) { return !!(value && typeof (value) === "object" && Number.isFinite(value.length)); } /** * Helper to reproduce index behavior of slice, indexOf, etc. * @private */ function getStartIndex(length, index) { if (typeof (index) !== "number" || index !== index) { return 0; } else if (index < 0) { return Math.max(0, length + index); } else { return index; } } /** * Helper to reproduce index behavior of slice, lastIndexOf, etc. * @private */ function getEndIndex(length, index) { if (typeof (index) !== "number" || index !== index) { return length; } else if (index < 0) { return length + index; } else { return Math.min(length, index); } } /** * The SortedArray type is a subclass of the base JavaScript Array * type whose elements are kept in a sorted order. * * Methods like {@link SortedArray.insert} are provided as an * alternative to the typical array `push` method for inserting * new elements in sorted order. * * Some methods, such as {@link SortedArray.indexOf} provide * optimized implementations of Array methods, which may operate * more efficiently upon sorted lists. * * **Warning:** The SortedArray type does not stop you * from still calling methods like `push`, `shift`, or `splice`, * which may invalidate the assumptions that SortedArray methods * make about array contents being correctly sorted. * These methods should be used with care, if they are used at all. * Invalidating the sort order of a SortedArray will cause many * operations to behave in unexpected ways. */ class SortedArray extends Array { constructor() { let values = undefined; let equalityFunc = undefined; let compareFunc = undefined; let reversedCompareFunc = undefined; // new SortedArray(compareFunc) if (arguments.length === 1 && typeof (arguments[0]) === "function") { compareFunc = arguments[0]; } // new SortedArray(compareFunc, equalityFunc) else if (arguments.length === 2 && typeof (arguments[0]) === "function" && typeof (arguments[1]) === "function") { compareFunc = arguments[0]; equalityFunc = arguments[1]; } // new SortedArray(values, compareFunc?, equalityFunc?) else { values = arguments[0]; compareFunc = arguments[1]; equalityFunc = arguments[2]; } if (compareFunc && typeof (compareFunc) !== "function") { throw new TypeError("Comparator argument is not a function."); } if (equalityFunc && typeof (equalityFunc) !== "function") { throw new TypeError("Equality argument is not a function."); } // new SortedArray(length, cmp?, eq?) - needed by some inherited methods if (typeof (values) === "number") { if (!Number.isInteger(values) || values < 0) { throw new RangeError("Invalid array length."); } super(values); } // new SortedArray(SortedArray, cmp?, eq?) - same or unspecified compareFunc else if (values instanceof SortedArray && (!compareFunc || values.compareFunc === compareFunc)) { super(); super.push(...values); compareFunc = values.compareFunc; reversedCompareFunc = values.reversedCompareFunc; if (!equalityFunc) { equalityFunc = values.equalityFunc; } } // new SortedArray(Array, cmp?, eq?) else if (Array.isArray(values)) { super(); super.push(...values); super.sort(compareFunc || exports.SortedArrayDefaultComparatorFunction); if (values instanceof SortedArray && !equalityFunc) { equalityFunc = values.equalityFunc; } } // new SortedArray(iterable, cmp?, eq?) else if (values && typeof (values[Symbol.iterator]) === "function") { super(); for (let value of values) { super.push(value); } super.sort(compareFunc || exports.SortedArrayDefaultComparatorFunction); } // new SortedArray(object with length, cmp?, eq?) - e.g. `arguments` else if (values && typeof (values) === "object" && Number.isFinite(values.length)) { super(); for (let i = 0; i < values.length; i++) { super.push(values[i]); } super.sort(compareFunc || exports.SortedArrayDefaultComparatorFunction); // new SortedArray() // new SortedArray(compareFunc) // new SortedArray(compareFunc, equalityFunc) } else if (!values) { super(); } // new SortedArray(???) else { throw new TypeError("Values argument is not iterable or array-like."); } this.equalityFunc = equalityFunc || exports.SortedArrayDefaultEqualityFunction; this.compareFunc = compareFunc || exports.SortedArrayDefaultComparatorFunction; this.reversedCompareFunc = reversedCompareFunc; } /** * Construct a new SortedArray and insert the given values. * * @param values The values that should be inserted and sorted in * the newly constructed SortedArray. * * @returns the newly constructed SortedArray, containing the given * values in sorted order. */ static of(...values) { return new SortedArray(values); } /** * Construct a new SortedArray, initializing it with some values * that are already provided in sorted order. * * @param values The values that should be inserted in the newly * constructed SortedArray. They must be sorted according to * {@link SortedArray.DefaultComparatorFunction}, otherwise the * returned SortedArray may behave unexpectedly. * * @returns the newly constructed SortedArray, containing the given * assumed-sorted values. */ static ofSorted(...values) { const array = new SortedArray(); Array.prototype.push.apply(array, values); return array; } /** * Construct a new SortedArray and insert the values in an iterable * or array-like object. * * @param values The values that should be inserted and sorted in * the newly constructed SortedArray. * @param compareFunc A custom comparator function, to use instead of * {@link SortedArray.DefaultComparatorFunction}. * @param equalityFunc A custom equality function, to use instead of * {@link SortedArray.DefaultEqualityFunction}. * * @returns the newly constructed SortedArray, containing the given * values in sorted order. */ static from(values, compareFunc, equalityFunc) { return new SortedArray(values, compareFunc, equalityFunc); } /** * Construct a new SortedArray, initializing it with some values * in an iterable or array-like object that are already provided * in sorted order. * * @param values The values that should be inserted in the newly * constructed SortedArray. They must be sorted according to * the used comparator function, otherwise the returned SortedArray * may behave unexpectedly. * @param compareFunc A custom comparator function, to use instead of * {@link SortedArray.DefaultComparatorFunction}. * @param equalityFunc A custom equality function, to use instead of * {@link SortedArray.DefaultEqualityFunction}. * * @returns the newly constructed SortedArray, containing the given * assumed-sorted values. */ static fromSorted(values, compareFunc, equalityFunc) { const array = new SortedArray(compareFunc, equalityFunc); if (Array.isArray(values)) { Array.prototype.push.apply(array, values); } else if (isIterable(values)) { for (let value of values) { Array.prototype.push.call(array, value); } } else { for (let i = 0; i < values.length; i++) { Array.prototype.push.call(array, values[i]); } } return array; } /** * Insert a value into the array, maintaining sort order. * * You probably want to use this method instead of `push`. * * @param value The value to insert into the array. * * @returns the new length of the array. */ insert(value) { const index = this.lastInsertionIndexOf(value); this.splice(index, 0, value); return this.length; } /** * Insert an iterable of assumed-sorted values into the array. * * This should typically be more efficient than calling * {@link SortedArray.insert} in a loop. * * @param values An iterable of values to be sorted into the * array, which are already sorted according to this SortedArray's * same comparator. * * @returns The new length of the array. * * @throws TypeError if the input was not iterable. */ insertSorted(values) { // Optimized implementation for arrays and iterable array-like objects if (isArrayLike(values)) { // Exit immediately if the values array is empty if (values.length === 0) { return this.length; } // If the last element in the input precedes the first element // in the array, the input can be prepended in one go. const lastInsertionIndex = this.lastInsertionIndexOf(values[values.length - 1]); if (lastInsertionIndex === 0) { if (isIterable(values)) { Array.prototype.unshift.call(this, ...values); } else { Array.prototype.unshift.apply(this, new Array(values.length)); for (let i = 0; i < values.length; i++) { this[i] = values[i]; } } return this.length; } // If the first element would go in the same place in the array // as the last element, then it can be spliced in all at once. const firstInsertionIndex = this.lastInsertionIndexOf(values[0]); if (firstInsertionIndex === lastInsertionIndex) { if (isIterable(values)) { Array.prototype.splice.call(this, firstInsertionIndex, 0, ...values); } else { Array.prototype.splice.call(this, firstInsertionIndex, 0, ...(new Array(values.length))); for (let i = 0; i < values.length; i++) { this[i + firstInsertionIndex] = values[i]; } } return this.length; } // Array contents must be interlaced let insertIndex = 0; for (let valIndex = 0; valIndex < values.length; valIndex++) { const value = values[valIndex]; insertIndex = this.lastInsertionIndexOf(value, insertIndex); // If this element was at the end of the array, then every other // element of the input is too and they can be appended at once. if (insertIndex === this.length && valIndex < values.length - 1) { if (Array.isArray(values)) { this.push(...values.slice(valIndex)); } else { for (let i = valIndex; i < values.length; i++) { this.push(values[i]); } } return this.length; } else { this.splice(insertIndex++, 0, value); } } return this.length; } // Generalized implementation for any iterable else if (isIterable(values)) { let insertIndex = 0; for (let value of values) { insertIndex = this.lastInsertionIndexOf(value, insertIndex); this.splice(insertIndex++, 0, value); } return this.length; } // Produce an error if the input isn't an acceptable type. else { throw new TypeError("Values argument is not iterable or array-like."); } } /** * Remove the first equal value from the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Remove the first item in the array that is equal * to this input value. * * @returns `true` if a matching element was found and removed, * or `false` if no matching element was found. */ remove(value) { const index = this.indexOf(value); if (index >= 0) { super.splice(index, 1); return true; } else { return false; } } /** * Remove the last equal value from the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Remove the last item in the array that is equal * to this input value. * * @returns `true` if a matching element was found and removed, * or `false` if no matching element was found. */ removeLast(value) { const index = this.lastIndexOf(value); if (index >= 0) { super.splice(index, 1); return true; } else { return false; } } /** * Remove all equal values from the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Remove all items in the array that are equal * to this input value. * * @returns the number of removed array items. */ removeAll(value) { let index = this.firstInsertionIndexOf(value); let removedCount = 0; while (index < this.length && this.compareFunc(this[index], value) === 0) { if (this.equalityFunc(this[index], value)) { removedCount++; super.splice(index, 1); } else { index++; } } return removedCount; } /** * Remove all equal values from the array and return those removed * values as a new SortedArray. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Remove the last item in the array that is equal * to this input value. * * @returns the removed elements as a new SortedArray. */ getRemoveAll(value) { let index = this.firstInsertionIndexOf(value); const removed = new SortedArray(this.compareFunc, this.equalityFunc); removed.reversedCompareFunc = this.reversedCompareFunc; while (index < this.length && this.compareFunc(this[index], value) === 0) { if (this.equalityFunc(this[index], value)) { Array.prototype.push.call(removed, this[index]); super.splice(index, 1); } else { index++; } } return removed; } /** * Get all equal values in the array and return them as a new * SortedArray. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Return values in the array that are equal to this one. * * @returns the equivalent elements as a new SortedArray. */ getEqualValues(value) { let index = this.firstInsertionIndexOf(value); const equal = new SortedArray(this.compareFunc, this.equalityFunc); equal.reversedCompareFunc = this.reversedCompareFunc; while (index < this.length && this.compareFunc(this[index], value) === 0) { if (this.equalityFunc(this[index], value)) { Array.prototype.push.call(equal, this[index]); } index++; } return equal; } /** * Returns the first index at which a new value could be inserted * into the array and maintain its sort order. * This index will be before any other values with an equivalent sort * order, if there are any such values in the array. * * @param value The value to check for insertion index. * @param fromIndex Start comparing items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * @param endIndex Stop comparing items at this index, exclusive. * Defaults to the length of the array. * * @returns The first valid insertion index for a new value. */ firstInsertionIndexOf(value, fromIndex, endIndex) { const from = getStartIndex(this.length, fromIndex); const end = getEndIndex(this.length, endIndex); let min = from - 1; let max = end; while (1 + min < max) { const mid = min + Math.floor((max - min) / 2); const cmp = this.compareFunc(value, this[mid]); if (cmp > 0) { min = mid; } else { max = mid; } } return max; } /** * Returns the last index at which a new value could be inserted * into the array and maintain its sort order. * This index will be after any other values with an equivalent sort * order, if there are any such values in the array. * * @param value The value to check for insertion index. * @param fromIndex Start comparing items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * @param endIndex Stop comparing items at this index, exclusive. * Defaults to the length of the array. * * @returns The last valid insertion index for a new value. */ lastInsertionIndexOf(value, fromIndex, endIndex) { const from = getStartIndex(this.length, fromIndex); const end = getEndIndex(this.length, endIndex); let min = from - 1; let max = end; while (1 + min < max) { const mid = min + Math.floor((max - min) / 2); const cmp = this.compareFunc(value, this[mid]); if (cmp >= 0) { min = mid; } else { max = mid; } } return max; } /** * Get the index of the first item in the array that is equal to * the given value, or `-1` if there is no equal item in the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Find the first index of a value in the array that * is equal to this one. * @param fromIndex Start looking for items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * * @returns the index of the first equal item, or `-1` if no equal * item was found. */ indexOf(value, fromIndex) { return this.indexOfRange(value, fromIndex); } /** * Get the index of the first item in the array that is equal to * the given value, or `-1` if there is no equal item in the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Find the first index of a value in the array that * is equal to this one. * @param fromIndex Start looking for items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * @param endIndex Stop looking for items at this index, exclusive. * Defaults to the length of the array. * * @returns the index of the first equal item, or `-1` if no equal * item was found. */ indexOfRange(value, fromIndex, endIndex) { let index = this.firstInsertionIndexOf(value, fromIndex, endIndex); if (index >= 0 && index < this.length && this.equalityFunc(this[index], value)) { return index; } const end = getEndIndex(this.length, endIndex); while (++index < end && this.compareFunc(value, this[index]) === 0) { if (this.equalityFunc(this[index], value)) { return index; } } return -1; } /** * Get the index of the last item in the array that is equal to * the given value, or `-1` if there is no equal item in the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * Note that the behavior of this method differs from the normal * Array `lastIndexOf` method in that passing `undefined` explicitly * as the `endIndex` argument is treated the same as omitting that * argument, instead of coercing the value to `0`. * * @param value Find the last index of a value in the array that * is equal to this one. * @param endIndex Stop looking for items at this index. * Defaults to the end of the array. * * @returns the index of the last equal item, or `-1` if no equal * item was found. */ lastIndexOf(value, endIndex) { return this.lastIndexOfRange(value, 0, endIndex); } /** * Get the index of the last item in the array that is equal to * the given value, or `-1` if there is no equal item in the array. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Find the last index of a value in the array that * is equal to this one. * @param fromIndex Start looking for items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * @param endIndex Stop looking for items at this index, exclusive. * Defaults to the length of the array. * * @returns the index of the last equal item, or `-1` if no equal * item was found. */ lastIndexOfRange(value, fromIndex, endIndex) { let index = this.lastInsertionIndexOf(value, fromIndex, endIndex); if (index >= 0 && index < this.length && this.equalityFunc(this[index], value)) { return index; } const from = getStartIndex(this.length, fromIndex); while (--index >= from && this.compareFunc(value, this[index]) === 0) { if (this.equalityFunc(this[index], value)) { return index; } } return -1; } /** * Check whether any item in the array is equal to the given value. * * Equality is determined using the SortedArray's equality function, * which is {@link SortedArray.DefaultEqualityFunction} when not * otherwise specified. * * @param value Check whether there is any value in the array that * is equal to this one. * @param fromIndex Start looking for items at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * * @returns `true` if an equal item was found in the array, or `false` * if not. */ includes(value, fromIndex) { return this.indexOf(value, fromIndex) >= 0; } /** * Get a new SortedArray containing only those items which satisfy * a given predicate function. * * @params predicate A callback function which is invoked for each * item in the array. The returned array will contain only those * items for which the callback returned a truthy value. * @params thisArg An optional `this` argument which the predicate * function should be called with. * * @returns a new SortedArray containing only those values which * satisfied the predicate function. */ filter(predicate, thisArg) { if (typeof (predicate) !== "function") { throw new TypeError("Predicate is not a function."); } let index = 0; const array = new SortedArray(this.compareFunc, this.equalityFunc); array.reversedCompareFunc = this.reversedCompareFunc; for (let element of this) { if (predicate.call(thisArg, element, index++, this)) { Array.prototype.push.call(array, element); } } return array; } /** * Reverse the items in the array, and update the array's comparator * function to account for this new reversed sort order. * Later insertions into this SortedArray will respect the reversed * sort order. * * @returns this SortedArray. */ reverse() { super.reverse(); if (this.reversedCompareFunc) { const t = this.compareFunc; this.compareFunc = this.reversedCompareFunc; this.reversedCompareFunc = t; } else { const t = this.compareFunc; this.reversedCompareFunc = this.compareFunc; this.compareFunc = (a, b) => t(b, a); } return this; } /** * Get the items in the array from a start to an end index as a new * SortedArray. * * @param start Get items starting at this index, inclusive. * Defaults to `0`, i.e. the beginning of the array. * @param end Get items ending at this index, exclusive. * Defaults to the length of the array. * * @returns a new SortedArray containing the given array slice. */ slice(start, end) { const slice = Array.prototype.slice.call(this, start, end); slice.equalityFunc = this.equalityFunc; slice.compareFunc = this.compareFunc; slice.reversedCompareFunc = this.reversedCompareFunc; return slice; } /** * Re-sort the array and assign a new comparator function. * * The array items will be sorted using the Array `sort` function. * This is not guaranteed to be stable in all cases. See: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#sort_stability * * @param compareFunc The new comparator function to use for * this SortedArray. * Uses {@link SortedArray.DefaultComparatorFunction} by default, * if no comparator function is specified. * * @returns this SortedArray. */ // @ts-ignore sort(compareFunc) { compareFunc = compareFunc || exports.SortedArrayDefaultComparatorFunction; if (compareFunc === this.compareFunc) { return this; } this.compareFunc = compareFunc; this.reversedCompareFunc = undefined; super.sort(compareFunc); return this; } /** * Forcibly re-sort the array using its previously assigned comparator * function. This might be used to repair the array, if its items are * no longer guaranteed to be in correct sorted order. * * The array items will be sorted using the Array `sort` function. * This is not guaranteed to be stable in all cases. See: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#sort_stability * * @returns this SortedArray. */ resort() { super.sort(this.compareFunc); return this; } /** * Check whether the contents of the array are in fact sorted as * expected. * * @returns `true` when the contents of the array are correctly * sorted according to its comparator function, or `false` otherwise. */ isSorted() { for (let i = 1; i < this.length; i++) { if (this.compareFunc(this[i - 1], this[i]) > 0) { return false; } } return true; } splice(start, deleteCount, ...items) { const splice = Array.prototype.splice.call( // @ts-ignore this, start, deleteCount, ...items); splice.equalityFunc = this.equalityFunc; splice.compareFunc = this.compareFunc; splice.reversedCompareFunc = this.reversedCompareFunc; return splice; } /** * Get a new array with the values in this array concatenated with * any number of other arrays. * * This method is overridden to ensure that a normal Array is returned, * instead of a SortedArray. It does not otherwise change the method's * behavior. * * @param items Arrays to concatenate. * * @returns a new array containing the concatenated items. */ concat(...items) { this.constructor = Array; // @ts-ignore const array = Array.prototype.concat.apply(this, items); this.constructor = SortedArray; return array; } /** * Get a new array with sub-array items concatenated into it * recursively, up to the specified depth. * * This method is overridden to ensure that a normal Array is returned, * instead of a SortedArray. It does not otherwise change the method's * behavior. * * @param depth How deep a nested array structure should be flattened. * Defaults to `1`. * * @returns a new array containing the flattened items. */ flat(depth) { this.constructor = Array; // @ts-ignore const array = Array.prototype.flat.call(this, depth); this.constructor = SortedArray; return array; } /** * Get a new array formed by applying a given transformation callback * function to each item in the array, and then flattening the result * by one level. * * This method is overridden to ensure that a normal Array is returned, * instead of a SortedArray. It does not otherwise change the method's * behavior. * * @param transform A transformation callback function to be invoked * for each item in the array. It should return an array containing new * items for the new array, or a single non-array value to be added to * the new array. * * @returns a new array containing the mapped and flattened items. */ // @ts-ignore flatMap(transform, thisArg) { this.constructor = Array; // @ts-ignore const array = Array.prototype.flatMap.call(this, transform, thisArg); this.constructor = SortedArray; return array; } /** * Get a new array populated with the results of calling a transformation * callback function on every item in this array. * * This method is overridden to ensure that a normal Array is returned, * instead of a SortedArray. It does not otherwise change the method's * behavior. * * @param transform A transformation callback function to be invoked * for each item in the array. Its return value is added as a single * item in the new array. * * @returns a new array containing the values returned by calls to * the transformation function. */ // @ts-ignore map(transform, thisArg) { this.constructor = Array; // @ts-ignore const array = Array.prototype.map.call(this, transform, thisArg); this.constructor = SortedArray; return array; } } exports.SortedArray = SortedArray; (function (SortedArray) { /** * Default equality function used by {@link SortedArray} instances, * when no other function was provided. * * The default equality function uses `SameValueZero` comparison, * i.e. strict equality using the `===` triple equals operator. * * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness */ SortedArray.DefaultEqualityFunction = exports.SortedArrayDefaultEqualityFunction; /** * Default comparator function used by {@link SortedArray} instances, * when no other function was provided. * * The default comparator function returns `-1` when `a < b`, * `+1` when `a > b`, and `0` otherwise. */ SortedArray.DefaultComparatorFunction = exports.SortedArrayDefaultComparatorFunction; })(SortedArray || (exports.SortedArray = SortedArray = {})); exports.default = SortedArray; //# sourceMappingURL=index.js.map