@barchart/common-js
Version:
Library of common JavaScript utilities
537 lines (440 loc) • 11.9 kB
JavaScript
import * as assert from './assert.js';
import * as is from './is.js';
/**
* Utilities for working with arrays.
*
* @public
* @module lang/array
*/
/**
* Returns the unique items from an array, where the unique
* key is determined via a strict equality check.
*
* @static
* @param {Array} a
* @returns {Array}
*/
export function unique(a) {
assert.argumentIsArray(a, 'a');
return uniqueBy(a, item => item);
}
/**
* Returns the unique items from an array, where the unique
* key is determined by a delegate.
*
* @static
* @param {Array} a
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function uniqueBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
return a.filter((item, index, array) => {
const key = keySelector(item);
return array.findIndex(candidate => key === keySelector(candidate)) === index;
});
}
/**
* Splits array into groups and returns an object (where the properties have
* arrays). Unlike the indexBy function, there can be many items which share
* the same key.
*
* @static
* @param {Array} a
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {object}
*/
export function groupBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
return a.reduce((groups, item) => {
const key = keySelector(item);
if (!Object.prototype.hasOwnProperty.call(groups, key)) {
groups[key] = [ ];
}
groups[key].push(item);
return groups;
}, { });
}
/**
* Splits array into groups and returns an array of arrays where the items of each
* nested array share a common key.
*
* @static
* @param {Array} a
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function batchBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
let currentKey = null;
let currentBatch = null;
return a.reduce((batches, item) => {
const key = keySelector(item);
if (currentBatch === null || currentKey !== key) {
currentKey = key;
currentBatch = [];
batches.push(currentBatch);
}
currentBatch.push(item);
return batches;
}, [ ]);
}
/**
* Splits array into groups and returns an object (where the properties are items from the
* original array). Unlike the {@link array#groupBy} function, only one item can have a
* given key value.
*
* @static
* @param {Array} a
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {object}
*/
export function indexBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
return a.reduce((map, item) => {
const key = keySelector(item);
if (Object.prototype.hasOwnProperty.call(map, key)) {
throw new Error('Unable to index array. A duplicate key exists.');
}
map[key] = item;
return map;
}, { });
}
/**
* Returns a new array containing all but the first item.
*
* @static
* @param {Array} a
* @returns {Array}
*/
export function dropLeft(a) {
assert.argumentIsArray(a, 'a');
let returnRef = Array.from(a);
if (returnRef.length !== 0) {
returnRef.shift();
}
return returnRef;
}
/**
* Returns a new array containing all but the last item.
*
* @static
* @param {Array} a
* @returns {Array}
*/
export function dropRight(a) {
assert.argumentIsArray(a, 'a');
let returnRef = Array.from(a);
if (returnRef.length !== 0) {
returnRef.pop();
}
return returnRef;
}
/**
* Returns the first item from an array, or an undefined value, if the
* array is empty.
*
* @static
* @param {Array} a
* @returns {*|undefined}
*/
export function first(a) {
assert.argumentIsArray(a, 'a');
let returnRef;
if (a.length !== 0) {
returnRef = a[0];
} else {
returnRef = undefined;
}
return returnRef;
}
/**
* Returns the last item from an array, or an undefined value, if the
* array is empty.
*
* @static
* @param {Array} a
* @returns {*|undefined}
*/
export function last(a) {
assert.argumentIsArray(a, 'a');
let returnRef;
if (a.length !== 0) {
returnRef = a[a.length - 1];
} else {
returnRef = undefined;
}
return returnRef;
}
/**
* Returns a copy of an array, replacing any item that is itself an array
* with the item's items.
*
* @static
* @param {Array} a
* @param {boolean=} recursive - If true, all nested arrays will be flattened.
* @returns {Array}
*/
export function flatten(a, recursive) {
assert.argumentIsArray(a, 'a');
assert.argumentIsOptional(recursive, 'recursive', Boolean);
const empty = [];
let flat = empty.concat.apply(empty, a);
if (recursive && flat.some(x => is.array(x))) {
flat = flatten(flat, true);
}
return flat;
}
/**
* Breaks an array into smaller arrays, returning an array of arrays.
*
* @static
* @param {Array} a
* @param {number} size - The maximum number of items per partition.
* @returns {Array<Array>}
*/
export function partition(a, size) {
assert.argumentIsArray(a, 'a');
assert.argumentIsOptional(size, 'size', Number);
const copy = a.slice(0);
const partitions = [ ];
while (copy.length !== 0) {
partitions.push(copy.splice(0, size));
}
return partitions;
}
/**
* Set difference operation, returning any item in "a" that is not
* contained in "b" (using strict equality).
*
* @static
* @param {Array} a
* @param {Array} b
* @returns {Array}
*/
export function difference(a, b) {
return differenceBy(a, b, item => item);
}
/**
* Set difference operation, returning any item in "a" that is not
* contained in "b" (where the uniqueness is determined by a delegate).
*
* @static
* @param {Array} a
* @param {Array} b
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function differenceBy(a, b, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsArray(b, 'b');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
const returnRef = [ ];
a.forEach((candidate) => {
const candidateKey = keySelector(candidate);
const exclude = b.some(comparison => candidateKey === keySelector(comparison));
if (!exclude) {
returnRef.push(candidate);
}
});
return returnRef;
}
/**
* Set symmetric difference operation (using strict equality). In
* other words, this is the union of the differences between the
* sets.
*
* @static
* @param {Array} a
* @param {Array} b
* @returns {Array}
*/
export function differenceSymmetric(a, b) {
return differenceSymmetricBy(a, b, item => item);
}
/**
* Set symmetric difference operation, where the uniqueness is determined by a delegate.
*
* @static
* @param {Array} a
* @param {Array} b
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function differenceSymmetricBy(a, b, keySelector) {
return unionBy(differenceBy(a, b, keySelector), differenceBy(b, a, keySelector), keySelector);
}
/**
* Set union operation (using strict equality).
*
* @static
* @param {Array} a
* @param {Array} b
* @returns {Array}
*/
export function union(a, b) {
return unionBy(a, b, item => item);
}
/**
* Set union operation, where the uniqueness is determined by a delegate.
*
* @static
* @param {Array} a
* @param {Array} b
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function unionBy(a, b, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsArray(b, 'b');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
const returnRef = a.slice();
b.forEach((candidate) => {
const candidateKey = keySelector(candidate);
const exclude = returnRef.some(comparison => candidateKey === keySelector(comparison));
if (!exclude) {
returnRef.push(candidate);
}
});
return returnRef;
}
/**
* Set intersection operation (using strict equality).
*
* @static
* @param {Array} a
* @param {Array} b
* @returns {Array}
*/
export function intersection(a, b) {
return intersectionBy(a, b, item => item);
}
/**
* Set intersection operation, where the uniqueness is determined by a delegate.
*
* @static
* @param {Array} a
* @param {Array} b
* @param {Function} keySelector - A function that returns a unique key for an item.
* @returns {Array}
*/
export function intersectionBy(a, b, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsArray(b, 'b');
const returnRef = [ ];
a.forEach((candidate) => {
const candidateKey = keySelector(candidate);
const include = b.some(comparison => candidateKey === keySelector(comparison));
if (include) {
returnRef.push(candidate);
}
});
return returnRef;
}
/**
* Removes the first item from an array which matches a predicate.
*
* @static
* @public
* @template T
* @param {T[]} a
* @param {(value: T, index: number, array: T[]) => boolean} predicate
* @returns {boolean}
*/
export function remove(a, predicate) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(predicate, 'predicate', Function);
const index = a.findIndex(predicate);
const found = !(index < 0);
if (found) {
a.splice(index, 1);
}
return found;
}
/**
* Inserts an item into an array using a binary search is used to determine the
* proper point for insertion and returns the same array.
*
* @static
* @public
* @param {Array} a
* @param {*} item
* @param {(a: any, b: any) => number} comparator
* @returns {Array}
*/
export function insert(a, item, comparator) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(comparator, 'comparator', Function);
if (a.length === 0 || !(comparator(item, a[a.length - 1]) < 0)) {
a.push(item);
} else if (comparator(item, a[0]) < 0) {
a.unshift(item);
} else {
a.splice(binarySearchForInsert(a, item, comparator, 0, a.length - 1), 0, item);
}
return a;
}
/**
* Performs a binary search to locate an item within an array.
*
* @param {*[]} a
* @param {*} key
* @param {Function} comparator
* @param {number=} start
* @param {number=} end
* @returns {*|null}
*/
export function binarySearch(a, key, comparator, start, end) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(comparator, 'comparator', Function);
assert.argumentIsOptional(start, 'start', Number);
assert.argumentIsOptional(end, 'end', Number);
if (a.length === 0) {
return null;
}
return binarySearchForMatch(a, key, comparator, start || 0, end || a.length - 1);
}
function binarySearchForMatch(a, key, comparator, start, end) {
const size = end - start;
const midpointIndex = start + Math.floor(size / 2);
const midpointItem = a[ midpointIndex ];
const comparison = comparator(key, midpointItem);
if (comparison === 0) {
return midpointItem;
} else if (size < 2) {
const finalIndex = a.length - 1;
const finalItem = a[ finalIndex ];
if (end === finalIndex && comparator(key, finalItem) === 0) {
return finalItem;
} else {
return null;
}
} else if (comparison > 0) {
return binarySearchForMatch(a, key, comparator, midpointIndex, end);
} else {
return binarySearchForMatch(a, key, comparator, start, midpointIndex);
}
}
function binarySearchForInsert(a, item, comparator, start, end) {
const size = end - start;
const midpointIndex = start + Math.floor(size / 2);
const midpointItem = a[midpointIndex];
const comparison = comparator(item, midpointItem);
if (size < 2) {
if (comparison > 0) {
const finalIndex = a.length - 1;
if (end === finalIndex && comparator(item, a[finalIndex]) > 0) {
return end + 1;
}
return end;
}
return start;
}
if (comparison > 0) {
return binarySearchForInsert(a, item, comparator, midpointIndex, end);
}
return binarySearchForInsert(a, item, comparator, start, midpointIndex);
}