@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
500 lines (499 loc) • 13.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports._chunk = _chunk;
exports._uniq = _uniq;
exports._pushUniq = _pushUniq;
exports._pushUniqBy = _pushUniqBy;
exports._uniqBy = _uniqBy;
exports._by = _by;
exports._mapBy = _mapBy;
exports._groupBy = _groupBy;
exports._sortBy = _sortBy;
exports._sortDescBy = _sortDescBy;
exports._find = _find;
exports._findLast = _findLast;
exports._takeWhile = _takeWhile;
exports._takeRightWhile = _takeRightWhile;
exports._dropWhile = _dropWhile;
exports._dropRightWhile = _dropRightWhile;
exports._countAtLeast = _countAtLeast;
exports._countLessThan = _countLessThan;
exports._count = _count;
exports._countBy = _countBy;
exports._intersection = _intersection;
exports._intersectsWith = _intersectsWith;
exports._difference = _difference;
exports._sum = _sum;
exports._sumBy = _sumBy;
exports._mapToObject = _mapToObject;
exports._shuffle = _shuffle;
exports._last = _last;
exports._lastOrUndefined = _lastOrUndefined;
exports._first = _first;
exports._minOrUndefined = _minOrUndefined;
exports._min = _min;
exports._maxOrUndefined = _maxOrUndefined;
exports._max = _max;
exports._maxBy = _maxBy;
exports._minBy = _minBy;
exports._maxByOrUndefined = _maxByOrUndefined;
exports._minByOrUndefined = _minByOrUndefined;
exports._zip = _zip;
const assert_1 = require("../error/assert");
const types_1 = require("../types");
/**
* Creates an array of elements split into groups the length of size. If collection can’t be split evenly, the
* final chunk will be the remaining elements.
*
* @param array The array to process.
* @param size The length of each chunk.
* @return Returns the new array containing chunks.
*
* https://lodash.com/docs#chunk
*
* Based on: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_chunk
*/
function _chunk(array, size = 1) {
const a = [];
for (let i = 0; i < array.length; i += size) {
a.push(array.slice(i, i + size));
}
return a;
}
/**
* Removes duplicates from given array.
*/
function _uniq(a) {
return [...new Set(a)];
}
/**
* Pushes an item to an array if it's not already there.
* Mutates the array (same as normal `push`) and also returns it for chaining convenience.
*
* _pushUniq([1, 2, 3], 2) // => [1, 2, 3]
*
* Shortcut for:
* if (!a.includes(item)) a.push(item)
* // or
* a = [...new Set(a).add(item)]
* // or
* a = _uniq([...a, item])
*/
function _pushUniq(a, ...items) {
for (const item of items) {
if (!a.includes(item))
a.push(item);
}
return a;
}
/**
* Like _pushUniq but uses a mapper to determine uniqueness (like _uniqBy).
* Mutates the array (same as normal `push`).
*/
function _pushUniqBy(a, mapper, ...items) {
const mappedSet = new Set(a.map((item, i) => mapper(item, i)));
items.forEach((item, i) => {
const mapped = mapper(item, i);
if (!mappedSet.has(mapped)) {
a.push(item);
mappedSet.add(mapped);
}
});
return a;
}
/**
* This method is like `_.uniq` except that it accepts `iteratee` which is
* invoked for each element in `array` to generate the criterion by which
* uniqueness is computed. The iteratee is invoked with one argument: (value).
*
* @returns Returns the new duplicate free array.
* @example
*
* _.uniqBy([2.1, 1.2, 2.3], Math.floor);
* // => [2.1, 1.2]
*
* // using the `_.property` iteratee shorthand
* _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
* // => [{ 'x': 1 }, { 'x': 2 }]
*
* Based on: https://stackoverflow.com/a/40808569/4919972
*/
function _uniqBy(arr, mapper) {
const map = new Map();
for (const [i, item] of arr.entries()) {
const key = item === undefined || item === null ? item : mapper(item, i);
if (!map.has(key))
map.set(key, item);
}
return [...map.values()];
}
/**
* const a = [
* {id: 'id1', a: 'a1'},
* {id: 'id2', b: 'b1'},
* ]
*
* _by(a, r => r.id)
* // => {
* id1: {id: 'id1', a: 'a1'},
* id2: {id: 'id2', b: 'b1'},
* }
*
* _by(a, r => r.id.toUpperCase())
* // => {
* ID1: {id: 'id1', a: 'a1'},
* ID2: {id: 'id2', b: 'b1'},
* }
*
* Returning `undefined` from the Mapper will EXCLUDE the item.
*/
function _by(items, mapper) {
return Object.fromEntries(items.map((item, i) => [mapper(item, i), item]).filter(([k]) => k !== undefined));
}
/**
* Map an array of items by a key, that is calculated by a Mapper.
*/
function _mapBy(items, mapper) {
return new Map(items.map((item, i) => [mapper(item, i), item]).filter(([k]) => k !== undefined));
}
/**
* const a = [1, 2, 3, 4, 5]
*
* _groupBy(a, r => r % 2 ? 'even' : 'odd')
* // => {
* odd: [1, 3, 5],
* even: [2, 4],
* }
*
* Returning `undefined` from the Mapper will EXCLUDE the item.
*/
function _groupBy(items, mapper) {
const map = {};
for (const [i, item] of items.entries()) {
const key = mapper(item, i);
if (key === undefined)
continue;
(map[key] ||= []).push(item);
}
return map;
}
/**
* _sortBy([{age: 20}, {age: 10}], 'age')
* // => [{age: 10}, {age: 20}]
*
* Same:
* _sortBy([{age: 20}, {age: 10}], o => o.age)
*/
function _sortBy(items, mapper, mutate = false, dir = 'asc') {
const mod = dir === 'desc' ? -1 : 1;
return (mutate ? items : [...items]).sort((_a, _b) => {
const [a, b] = [_a, _b].map(mapper);
if (typeof a === 'number' && typeof b === 'number')
return (a - b) * mod;
return String(a).localeCompare(String(b)) * mod;
});
}
/**
* Alias for _sortBy with descending order.
*/
function _sortDescBy(items, mapper, mutate = false) {
return _sortBy(items, mapper, mutate, 'desc');
}
/**
* Similar to `Array.find`, but the `predicate` may return `END` to stop the iteration early.
*
* Use `Array.find` if you don't need to stop the iteration early.
*/
function _find(items, predicate) {
for (const [i, item] of items.entries()) {
const result = predicate(item, i);
if (result === types_1.END)
return;
if (result)
return item;
}
}
/**
* Similar to `Array.findLast`, but the `predicate` may return `END` to stop the iteration early.
*
* Use `Array.findLast` if you don't need to stop the iteration early, which is supported:
* - in Node since 18+
* - in iOS Safari since 15.4
*/
function _findLast(items, predicate) {
return _find(items.slice().reverse(), predicate);
}
function _takeWhile(items, predicate) {
let proceed = true;
return items.filter((v, index) => (proceed &&= predicate(v, index)));
}
function _takeRightWhile(items, predicate) {
let proceed = true;
return [...items].reverse().filter((v, index) => (proceed &&= predicate(v, index)));
}
function _dropWhile(items, predicate) {
let proceed = false;
return items.filter((v, index) => (proceed ||= !predicate(v, index)));
}
function _dropRightWhile(items, predicate) {
let proceed = false;
return [...items]
.reverse()
.filter((v, index) => (proceed ||= !predicate(v, index)))
.reverse();
}
/**
* Returns true if the _count >= limit.
* _count counts how many times the Predicate returns true, and stops
* when it reaches the limit.
*/
function _countAtLeast(items, predicate, limit) {
return _count(items, predicate, limit) >= limit;
}
/**
* Returns true if the _count <> limit.
* _count counts how many times the Predicate returns true, and stops
* when it reaches the limit.
*/
function _countLessThan(items, predicate, limit) {
return _count(items, predicate, limit) < limit;
}
/**
* Counts how many items match the predicate.
*
* `limit` allows to exit early when limit count is reached, skipping further iterations (perf optimization).
*/
function _count(items, predicate, limit) {
if (limit === 0)
return 0;
let count = 0;
let i = 0;
for (const item of items) {
const r = predicate(item, i++);
if (r === types_1.END)
break;
if (r) {
count++;
if (limit && count >= limit)
break;
}
}
return count;
}
function _countBy(items, mapper) {
const map = {};
let i = 0;
for (const item of items) {
const key = mapper(item, i++);
map[key] = (map[key] || 0) + 1;
}
return map;
}
// investigate: _groupBy
/**
* Returns an intersection between 2 arrays.
*
* Intersecion means an array of items that are present in both of the arrays.
*
* It's more performant to pass a Set as a second argument.
*
* @example
* _intersection([2, 1], [2, 3])
* // [2]
*/
function _intersection(a1, a2) {
const a2set = a2 instanceof Set ? a2 : new Set(a2);
return a1.filter(v => a2set.has(v));
}
/**
* Returns true if there is at least 1 item common between 2 arrays.
* Otherwise returns false.
*
* It's more performant to use that versus `_intersection(a1, a2).length > 0`.
*
* Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
*/
function _intersectsWith(a1, a2) {
const a2set = a2 instanceof Set ? a2 : new Set(a2);
return a1.some(v => a2set.has(v));
}
/**
* Returns array1 minus array2.
*
* @example
* _difference([2, 1], [2, 3])
* // [1]
*
* Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
*/
function _difference(a1, a2) {
const a2set = a2 instanceof Set ? a2 : new Set(a2);
return a1.filter(v => !a2set.has(v));
}
/**
* Returns the sum of items, or 0 for empty array.
*/
function _sum(items) {
let sum = 0;
for (const n of items) {
sum = (sum + n);
}
return sum;
}
function _sumBy(items, mapper) {
let sum = 0;
let i = 0;
for (const n of items) {
const v = mapper(n, i++);
if (typeof v === 'number') {
// count only numbers, nothing else
sum = (sum + v);
}
}
return sum;
}
/**
* Map an array of T to a StringMap<V>,
* by returning a tuple of [key, value] from a mapper function.
* Return undefined/null/false/0/void to filter out (not include) a value.
*
* @example
*
* _mapToObject([1, 2, 3], n => [n, n * 2])
* // { '1': 2, '2': 4, '3': 6 }
*
* _mapToObject([1, 2, 3], n => [n, `id${n}`])
* // { '1': 'id1, '2': 'id2', '3': 'id3' }
*/
function _mapToObject(array, mapper) {
const m = {};
for (const item of array) {
const r = mapper(item);
if (!r)
continue; // filtering
m[r[0]] = r[1];
}
return m;
}
/**
* Randomly shuffle an array values.
* Fisher–Yates algorithm.
* Based on: https://stackoverflow.com/a/12646864/4919972
*/
function _shuffle(array, mutate = false) {
const a = mutate ? array : [...array];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
/**
* Returns last item of non-empty array.
* Throws if array is empty.
*/
function _last(array) {
if (!array.length)
throw new Error('_last called on empty array');
return array[array.length - 1];
}
/**
* Returns last item of the array (or undefined if array is empty).
*/
function _lastOrUndefined(array) {
return array[array.length - 1];
}
/**
* Returns the first item of non-empty array.
* Throws if array is empty.
*/
function _first(array) {
if (!array.length)
throw new Error('_first called on empty array');
return array[0];
}
function _minOrUndefined(array) {
let min;
for (const item of array) {
if (item === undefined || item === null)
continue;
if (min === undefined || item < min) {
min = item;
}
}
return min;
}
/**
* Filters out nullish values (undefined and null).
*/
function _min(array) {
const min = _minOrUndefined(array);
(0, assert_1._assert)(min !== undefined, '_min called on empty array');
return min;
}
function _maxOrUndefined(array) {
let max;
for (const item of array) {
if (item === undefined || item === null)
continue;
if (max === undefined || item > max) {
max = item;
}
}
return max;
}
/**
* Filters out nullish values (undefined and null).
*/
function _max(array) {
const max = _maxOrUndefined(array);
(0, assert_1._assert)(max !== undefined, '_max called on empty array');
return max;
}
function _maxBy(array, mapper) {
const max = _maxByOrUndefined(array, mapper);
(0, assert_1._assert)(max !== undefined, '_maxBy returned undefined');
return max;
}
function _minBy(array, mapper) {
const min = _minByOrUndefined(array, mapper);
(0, assert_1._assert)(min !== undefined, '_minBy returned undefined');
return min;
}
// todo: looks like it _maxByOrUndefined/_minByOrUndefined can be DRYer
function _maxByOrUndefined(array, mapper) {
if (!array.length)
return;
let maxItem;
let max;
for (const [i, item] of array.entries()) {
const v = mapper(item, i);
if (v !== undefined && (max === undefined || v > max)) {
maxItem = item;
max = v;
}
}
return maxItem;
}
function _minByOrUndefined(array, mapper) {
if (!array.length)
return;
let minItem;
let min;
for (const [i, item] of array.entries()) {
const v = mapper(item, i);
if (v !== undefined && (min === undefined || v < min)) {
minItem = item;
min = v;
}
}
return minItem;
}
function _zip(array1, array2) {
const len = Math.min(array1.length, array2.length);
const res = [];
for (let i = 0; i < len; i++) {
res.push([array1[i], array2[i]]);
}
return res;
}