@barchart/common-js
Version:
Library of common JavaScript utilities
538 lines (446 loc) • 12.5 kB
JavaScript
const assert = require('./assert'),
is = require('./is');
module.exports = (() => {
'use strict';
/**
* Utilities for working with arrays.
*
* @public
* @module lang/array
*/
return {
/**
* Returns the unique items from an array, where the unique
* key is determined via a strict equality check.
*
* @static
* @param {Array} a
* @returns {Array}
*/
unique(a) {
assert.argumentIsArray(a, 'a');
return this.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}
*/
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}
*/
groupBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
return a.reduce((groups, item) => {
const key = keySelector(item);
if (!groups.hasOwnProperty(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}
*/
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}
*/
indexBy(a, keySelector) {
assert.argumentIsArray(a, 'a');
assert.argumentIsRequired(keySelector, 'keySelector', Function);
return a.reduce((map, item) => {
const key = keySelector(item);
if (map.hasOwnProperty(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}
*/
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}
*/
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}
*/
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}
*/
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}
*/
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 = this.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.
* @param {Array<Array>}
*/
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}
*/
difference(a, b) {
return this.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}
*/
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}
*/
differenceSymmetric(a, b) {
return this.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}
*/
differenceSymmetricBy(a, b, keySelector) {
return this.unionBy(this.differenceBy(a, b, keySelector), this.differenceBy(b, a, keySelector), keySelector);
},
/**
* Set union operation (using strict equality).
*
* @static
* @param {Array} a
* @param {Array} b
* @returns {Array}
*/
union(a, b) {
return this.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}
*/
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}
*/
intersection(a, b) {
return this.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}
*/
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
* @param {Array} a
* @param {Function} predicate
* @returns {Boolean}
*/
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 {Function} comparator
* @returns {Array}
*/
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}
*/
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) > 0);
if (size < 2) {
if (comparison > 0) {
const finalIndex = a.length - 1;
if (end === finalIndex && comparator(item, a[ finalIndex ]) > 0) {
return end + 1;
} else {
return end;
}
} else {
return start;
}
} else if (comparison > 0) {
return binarySearchForInsert(a, item, comparator, midpointIndex, end);
} else {
return binarySearchForInsert(a, item, comparator, start, midpointIndex);
}
}
})();