atomic-fns
Version:
Like Lodash, but for ESNext and with types. Stop shipping code built for browsers from 2015.
720 lines (713 loc) • 21.7 kB
JavaScript
/**
* This module includes classes and functions to work with Container types.
*
* @module collections
*/
import { call, get, isArray, isArrayLike, isBool, isEmpty, isFunction, isIterable, isNull, isNumber, isObject, isString, keys } from '../globals/index.js';
import { enumerate } from '../itertools/index.js';
import { compare, eq, id } from '../operators/index.js';
export * from './abc.js';
export * from './BTree.js';
export * from './deque.js';
export * from './frozenset.js';
export * from './Heap.js';
export * from './LRUCache.js';
export * from './SortedMap.js';
export * from './SortedSet.js';
export * from './SortedTree.js';
export * from './SplayTree.js';
export * from './Trie.js';
export function compact(arr) {
if (!arr)
return;
if (Array.isArray(arr))
return arr.filter((x) => !isEmpty(x));
const result = {};
for (const key of Object.keys(arr)) {
if (!isEmpty(arr[key])) {
result[key] = arr[key];
}
}
return result;
}
export function count(obj, func) {
const counters = {};
forEach(obj, (value, key) => {
const result = func(value, key);
counters[result] = get(result, counters, 0) + 1;
});
return counters;
}
export function filter(arr, fn = isNull) {
if (!arr)
return;
const predicate = isFunction(fn) ? fn : isObject(fn) ? matches(fn) : (obj) => get(fn, obj);
const results = [];
forEach(arr, (value, key) => {
if (predicate(value, key, arr))
results.push(value);
});
return results;
}
export function find(arr, fn) {
const it = isFunction(fn) ? fn : isObject(fn) ? matches(fn) : (obj) => get(fn, obj);
if (Array.isArray(arr)) {
return arr.find(it);
}
else if (isObject(arr)) {
for (const key of Object.keys(arr)) {
if (it(arr[key], key, arr)) {
return arr[key];
}
}
}
}
/**
* Performs an efficient array insert operation in the given array. If the index or the array is invalid, it just returns the given array.
*
* **Note:** Uses the same behavior as `Array.splice`.
*
* @param {Array<*>} arr The given array to insert into
* @param {number} index The index of the array insert operation.
* @param {*} value The value to insert in the array at the given `index`.
* @returns {Array<*>} The given array.
*/
export function insert(arr, index, value) {
if (!arr || index < 0)
return arr;
arr.splice(index, 0, value);
return arr;
}
/**
* Creates a function that performs a partial deep comparison between a given object and `shape`, returning `true` if the given object has equivalent property values, else `false`.
* @example
```js
let objects = [
{ a: 1, b: 2, c: 3 },
{ a: 4, b: 5, c: 6 }
]
filter(objects, matches({ a: 4, c: 6 }))
// [{ a: 4, b: 5, c: 6 }]
```
* @param shape
* @returns
*/
export function matches(shape) {
return function (obj) {
if (!shape || !obj)
return false;
for (const key in shape) {
if (obj[key] !== shape[key])
return false;
}
return true;
};
}
export function forEach(collection, fn) {
if (Array.isArray(collection)) {
for (let i = 0; i < collection.length; i++) {
const res = fn(collection[i], i, collection);
if (res === false)
return;
}
}
else if (isIterable(collection)) {
for (const [index, value] of enumerate(collection)) {
const res = fn(value, index, collection);
if (res === false)
return;
}
}
else {
for (const key of keys(collection)) {
const res = fn(collection[key], key, collection);
if (res === false)
return;
}
}
}
export function findLast(arr, fn) {
let result;
const it = isFunction(fn) ? fn : matches(fn);
forEachRight(arr, (val, key) => {
if (it(val, key, arr)) {
result = val;
return false;
}
});
return result;
}
export function forEachRight(collection, fn) {
if (Array.isArray(collection)) {
for (let i = collection.length - 1; i >= 0; i--) {
const value = collection[i];
const res = fn(value, i, collection);
if (res === false)
return;
}
}
else if (isObject(collection)) {
const keys = Object.keys(collection);
for (let i = keys.length - 1; i >= 0; i--) {
const key = keys[i];
const value = collection[key];
const res = fn(value, key, collection);
if (res === false)
return;
}
}
}
export function flatten(arr, depth = 1) {
if (!depth)
return arr;
if (Array.isArray(arr))
return flattenArray(arr, depth, []);
if (isObject(arr))
return flattenObj(arr);
return arr;
}
function flattenArray(arr, depth = 1, result = []) {
for (const value of arr) {
if (depth && Array.isArray(value)) {
flattenArray(value, typeof depth === 'number' ? depth - 1 : depth, result);
}
else {
result.push(value);
}
}
return result;
}
function flattenObj(obj, prefix = '', result = {}, keepNull = false) {
if (isString(obj) || isNumber(obj) || isBool(obj) || (keepNull && isNull(obj))) {
result[prefix] = obj;
return result;
}
if (isArray(obj) || isObject(obj)) {
for (const i of Object.keys(obj)) {
let pref = prefix;
if (isArray(obj)) {
pref = `${pref}[${i}]`;
}
else {
if (prefix) {
pref = `${prefix}.${i}`;
}
else {
pref = i;
}
}
flattenObj(obj[i], pref, result, keepNull);
}
return result;
}
return result;
}
export function map(arr, fn) {
const it = isFunction(fn) ? fn : (obj) => get(fn, obj);
if (Array.isArray(arr)) {
return arr.map((x, i) => it(x, i, arr));
}
if (isObject(arr)) {
return Object.keys(arr).map((key) => it(arr[key], key, arr));
}
}
export function pick(obj, paths) {
if (!obj)
return {};
const result = {};
if (isFunction(paths)) {
for (const key of Object.keys(obj)) {
if (paths(obj[key], key)) {
result[key] = obj[key];
}
}
return result;
}
for (const key of paths) {
result[key] = get(key, obj);
}
return result;
}
export function omit(obj, paths) {
if (!obj)
return {};
const result = {};
if (isFunction(paths)) {
for (const key of Object.keys(obj)) {
if (!paths(obj[key], key)) {
result[key] = obj[key];
}
}
return result;
}
Object.assign(result, obj);
for (const key of paths) {
delete result[key];
}
return result;
}
export function findIndex(obj, fn, start = 0) {
if (!obj)
return -1;
const length = obj.length;
if (length && start < 0) {
start = Math.max(start + length, 0);
}
if (!length || start >= length)
return -1;
const it = isFunction(fn) ? fn : isObject(fn) ? matches(fn) : (obj) => get(fn, obj);
for (; start < length; start++) {
if (it(obj[start], start, obj))
return start;
}
return -1;
}
/**
* This method is like {@link findIndex} except that it searches for a given value directly, instead of using a predicate function.
* @param {Array} obj The array to inspect.
* @param {*} value The value to find
* @param {number} [start=0] The index to search from.
* @returns {number} The index of the found value, else -1
* @example
```js
indexOf([1, 2, 1, 2], 2)
// 1
// Search from a `start` index.
indexOf([1, 2, 1, 2], 2, 2)
// 3
```
* @see {@link findIndex}
* @see {@link lastIndexOf}
*/
export function indexOf(obj, value, start = 0) {
if (!obj)
return -1;
const op = call(obj, 'indexOf', value, start);
if (op != null)
return op;
const length = obj.length;
if (length && start < 0) {
start = Math.max(start + length, 0);
}
if (!length || start >= length)
return -1;
for (; start < length; start++) {
if (eq(value, obj[start]))
return start;
}
return -1;
}
export function findLastIndex(arr, fn, start) {
if (arr == null)
return -1;
const length = arr.length;
if (start == null)
start = length - 1;
if (!length || start < 0)
return -1;
const it = isFunction(fn) ? fn : isObject(fn) ? matches(fn) : (obj) => get(fn, obj);
for (; start >= 0; start--) {
if (it(arr[start], start, arr))
return start;
}
return -1;
}
/**
* This method is like {@link indexOf} except that it iterates the collection from right to left.
* @param {Array} obj The array to inspect.
* @param {*} value The value to find
* @param {number} [start] The index to search from.
* @returns {number} The index of the found value, else -1
* @example
```js
lastIndexOf([1, 2, 1, 2], 2)
// 3
// Search from the `fromIndex`.
lastIndexOf([1, 2, 1, 2], 2, 2)
// 1
```
* @see {@link findLastIndex}
* @see {@link indexOf}
*/
export function lastIndexOf(obj, value, start) {
if (obj == null)
return -1;
const op = call(obj, 'lastIndexOf', value, start);
if (op != null)
return op;
const length = obj.length;
if (start == null)
start = length - 1;
if (!length || start < 0)
return -1;
for (; start >= 0; start--) {
if (eq(value, obj[start]))
return start;
}
return -1;
}
export function clone(value, deep = false) {
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
return cloneTypedArray(value, deep);
}
if (isArray(value) || isArrayLike(value)) {
return cloneArray(value, deep);
}
if (isObject(value)) {
if (isFunction(value.clone)) {
return value.clone();
}
const copy = {};
for (const key in value) {
if (deep) {
copy[key] = clone(value[key], deep);
}
else {
copy[key] = value[key];
}
}
return copy;
}
return value;
}
/**
* Clones an array. If `deep` is `false` (default) the clone will be shallow. Otherwise {@link https://developer.mozilla.org/en-US/docs/Web/API/structuredClone structuredClone} is used.
* @param arr The array to clone
* @param [deep=false] Creates a deep clone using `structuredClone` if true.
* @returns The new array
* @see {@link clone}
*/
export function cloneArray(arr, deep = false) {
if (!deep)
return Array.from(arr);
return structuredClone(arr);
}
/**
* Clones a typed array. If `deep` is `false` (default) the clone will be shallow. Otherwise {@link https://developer.mozilla.org/en-US/docs/Web/API/structuredClone structuredClone} is used.
* @param arr The array to clone
* @param [deep=false] Creates a deep clone using `structuredClone` if true.
* @returns The new array
* @see {@link clone}
*/
export function cloneTypedArray(typedArray, isDeep = false) {
const buffer = isDeep ? structuredClone(typedArray) : typedArray.buffer;
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
}
export function uniq(arr, fn = id) {
const keys = new Set();
const it = isFunction(fn) ? fn : (x) => get(fn, x);
for (const x of arr) {
keys.add(it(x));
}
return Array.from(keys.values());
}
export function sortedUniq(arr, fn = id) {
const values = uniq(arr, fn);
values.sort(compare);
return values;
}
function baseMergeDeep(obj, source, key, stack) {
const objValue = obj[key];
const srcValue = source[key];
const stacked = stack.get(srcValue);
if (stacked && (objValue !== stacked || !(key in obj))) {
obj[key] = stacked;
return;
}
let newValue;
let isCommon = newValue === undefined;
const isArray = Array.isArray(srcValue);
const isTyped = ArrayBuffer.isView(srcValue);
if (isArray || isTyped) {
if (Array.isArray(objValue)) {
newValue = objValue;
}
else if (isArrayLike(objValue)) {
newValue = Array.from(objValue);
}
else if (isTyped) {
isCommon = false;
newValue = cloneTypedArray(srcValue, true);
}
else {
newValue = [];
}
}
else if (isObject(srcValue)) {
newValue = objValue;
if (!isObject(objValue)) {
newValue = srcValue;
}
}
else {
isCommon = false;
}
if (isCommon) {
stack.set(srcValue, newValue);
baseMerge(newValue, srcValue, stack);
stack.delete(srcValue);
}
obj[key] = newValue;
}
function baseMerge(obj, source, stack) {
if (obj === source)
return obj;
for (const key of Object.keys(source)) {
const srcValue = source[key];
const objValue = obj[key];
if (typeof srcValue === 'object' && srcValue !== null) {
baseMergeDeep(obj, source, key, stack || new WeakMap());
}
else {
if (objValue !== srcValue) {
obj[key] = srcValue;
}
}
}
}
/**
* Recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. Source properties that resolve to `undefined` are skipped if a destination value exists. Array and plain object properties are merged recursively. Other objects and value types are overridden by assignment. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
*
* Note: this method mutates `object`
*
*
* @param {Object} object The destination object.
* @param {...Object} sources The source objects.
* @returns Returns `object`.
*
* @example
```js
let object = {
'a': [{ 'b': 2 }, { 'd': 4 }]
}
let other = {
'a': [{ 'c': 3 }, { 'e': 5 }]
}
merge(object, other)
// { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
```
*/
export function merge(object, ...sources) {
for (const source of sources) {
baseMerge(object, source);
}
return object;
}
/**
* Assigns own and inherited enumerable string keyed properties of source objects to the destination object for all destination properties that resolve to `undefined`. Source objects are applied from left to right. Once a property is set, additional values of the same property are ignored.
* **Note:** This method mutates `object`.
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns Returns `object`.
* @example
```js
defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 })
// { 'a': 1, 'b': 2 }
```
*/
export function defaults(object, ...sources) {
for (const source of sources) {
for (const key in source) {
if (object[key] === undefined) {
object[key] = source[key];
}
}
}
return object;
}
/**
* Returns a generator of array values not included in the other given arrays using a `Set` for equality comparisons. The order and references of result values are not guaranteed.
* @param args The initial arrays
* @example
```js
[...difference([2, 1], [2, 3])]
// [1]
```
* @see {@link union}
* @see {@link intersection}
*/
export function* difference(...args) {
const sets = args.map((arr) => new Set(arr));
const setA = sets[0];
for (let i = 1; i < sets.length; i++) {
for (const x of sets[i].values()) {
if (!setA.delete(x))
setA.add(x);
}
}
for (const x of setA.values()) {
yield x;
}
}
/**
* Creates a generator of unique values that are included in all given arrays.
* @param args The arrays to inspect
* @example
```js
[...intersection([2, 1], [2, 3])]
// [2]
```
* @see {@link difference}
* @see {@link union}
*/
export function* intersection(...args) {
const sets = args.map((arr) => new Set(arr));
// build a counter map to find items in all
const results = new Map();
const total = sets.length;
for (let i = 0; i < total; i++) {
for (const x of sets[i].values()) {
const count = results.get(x) || 0;
results.set(x, count + 1);
}
}
for (const [key, value] of results.entries()) {
if (value === total)
yield key;
}
}
/**
* Creates a generator of unique values from all given arrays using `Set` for equality comparisons.
* @param args The arrays to perform union on.
* @example
```js
[...union([2], [1, 2])]
// [2, 1]
```
* @see {@link difference}
* @see {@link intersection}
*/
export function* union(...args) {
const results = new Set();
for (const arr of args) {
for (const item of arr) {
results.add(item);
}
}
for (const item of results.values()) {
yield item;
}
}
export function groupBy(arr, func = id) {
const useKey = isFunction(func) ? func : (obj) => get(func, obj);
const results = {};
forEach(arr, (value) => {
const groupKey = useKey(value);
const values = results[groupKey] || [];
values.push(value);
results[groupKey] = values;
});
return results;
}
export function groupByMap(arr, func = id) {
const results = new Map();
const useKey = isFunction(func) ? func : (obj) => get(func, obj);
forEach(arr, (value) => {
const groupKey = useKey(value);
const values = results.get(groupKey) || [];
values.push(value);
results.set(groupKey, values);
});
return results;
}
export function remove(arr, func) {
const it = isFunction(func) ? func : isArray(func) ? (x) => func.includes(x) : (x) => func === x;
return arr.filter((x, i) => !it(x, i, arr));
}
/**
* Returns the index of `x` in a **sorted** array if found, in `O(log n)` using binary search.
* If the element is not found, returns a negative integer.
* @param arr The array to sort
* @param x The element to find
* @param lo The starting index
* @param hi The end index to search within
* @param comp The compare function to check for `x`
* @returns {number} The index if the element is found or a negative integer.
*/
export function binarySearch(arr, x, lo = 0, hi, comp = compare) {
hi = hi ?? arr.length - 1;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const check = comp(x, arr[mid]);
if (check === 0)
return mid;
if (check > 0)
lo = mid + 1;
else
hi = mid - 1;
}
if (comp(x, arr[lo]) === 0)
return lo;
// Returns (-lo - 1) where n is the insertion point for new element in the range
return -lo - 1;
}
/**
* Returns an insertion index which comes after any existing entries of `x` in a **sorted** array, using binary search.
* @param arr The array to sort
* @param x The element to find
* @param lo The starting index
* @param hi The end index to search within
* @param comp The compare function to check for `x`
* @returns {number}
*/
export function bisect(arr, x, lo = 0, hi, comp = compare) {
hi = hi ?? arr.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const check = comp(x, arr[mid]);
if (check >= 0)
lo = mid + 1;
else
hi = mid - 1;
}
return lo;
}
/**
* Returns an insertion index which comes before any existing entries of `x` in a **sorted** array, using binary search.
* @param arr The array to sort
* @param x The element to find
* @param lo The starting index
* @param hi The end index to search within
* @param comp The compare function to check for `x`
* @returns {number}
*/
export function bisectLeft(arr, x, lo = 0, hi, comp = compare) {
hi = hi ?? arr.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const check = comp(x, arr[mid]);
if (check <= 0)
hi = mid - 1;
else
lo = mid + 1;
}
return lo;
}
/**
* Runs {@link bisect} first to locate an insertion point, and inserts the value `x` in the sorted array after any existing entries of `x` to maintain sort order.
* Please note this method is `O(n)` because insertion resizes the array.
* @param arr The array to insert into
* @param x The element to insert
* @param lo The starting index
* @param hi The end index to search within
* @param comp The compare function to check for `x`
* @returns {Array}
*/
export function insort(arr, x, lo = 0, hi, comp = compare) {
const index = bisect(arr, x, lo, hi, comp);
return insert(arr, index, x);
}
/**
* Runs {@link bisectLeft} first to locate an insertion point, and inserts the value `x` in the sorted array before any existing entries of `x` to maintain sort order.
* Please note this method is `O(n)` because insertion resizes the array.
* @param arr The array to insert into
* @param x The element to insert
* @param lo The starting index
* @param hi The end index to search within
* @param comp The compare function to check for `x`
* @returns {Array}
*/
export function insortLeft(arr, x, lo = 0, hi, comp = compare) {
const index = bisectLeft(arr, x, lo, hi, comp);
return insert(arr, index, x);
}