UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

480 lines (479 loc) 13.1 kB
import { _assert } from '../error/assert.js'; import { END } from '../types.js'; /** * 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 */ export 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. */ export 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]) */ export 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`). */ export function _pushUniqBy(a, mapper, ...items) { const mappedSet = new Set(a.map(mapper)); for (const item of items) { const mapped = mapper(item); 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 */ export function _uniqBy(arr, mapper) { const map = new Map(); for (let i = 0; i < arr.length; i++) { const item = arr[i]; const key = item === undefined || item === null ? item : mapper(item); 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. */ export function _by(items, mapper) { const map = {}; for (let i = 0; i < items.length; i++) { const v = items[i]; const k = mapper(v); if (k !== undefined) { map[k] = v; } } return map; } /** * Map an array of items by a key, that is calculated by a Mapper. */ export function _mapBy(items, mapper) { const map = new Map(); for (let i = 0; i < items.length; i++) { const item = items[i]; const key = mapper(item); if (key !== undefined) { map.set(key, item); } } return map; } /** * 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. */ export function _groupBy(items, mapper) { const map = {}; for (let i = 0; i < items.length; i++) { const item = items[i]; const key = mapper(item); if (key !== undefined) { ; (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) */ export function _sortBy(items, mapper, opt = {}) { const mod = opt.dir === 'desc' ? -1 : 1; return (opt.mutate ? items : [...items]).sort((_a, _b) => { // This implementation may call mapper more than once per item, // but the benchmarks show no significant difference in performance. const a = mapper(_a); const b = mapper(_b); // if (typeof a === 'number' && typeof b === 'number') return (a - b) * mod // return String(a).localeCompare(String(b)) * mod if (a > b) return mod; if (a < b) return -mod; return 0; }); } /** * 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. */ export function _find(items, predicate) { for (let i = 0; i < items.length; i++) { const item = items[i]; const result = predicate(item, i); if (result === 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 */ export function _findLast(items, predicate) { return _find(items.slice().reverse(), predicate); } export function _takeWhile(items, predicate) { let proceed = true; return items.filter((v, index) => (proceed &&= predicate(v, index))); } export function _takeRightWhile(items, predicate) { let proceed = true; return [...items].reverse().filter((v, index) => (proceed &&= predicate(v, index))); } export function _dropWhile(items, predicate) { let proceed = false; return items.filter((v, index) => (proceed ||= !predicate(v, index))); } export 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. */ export 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. */ export 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). */ export 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 === END) break; if (r) { count++; if (limit && count >= limit) break; } } return count; } export function _countBy(items, mapper) { const map = {}; for (const item of items) { const key = mapper(item); 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] */ export 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). */ export 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). */ export 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. */ export function _sum(items) { let sum = 0; for (const n of items) { sum = (sum + n); } return sum; } export function _sumBy(items, mapper) { let sum = 0; for (const n of items) { const v = mapper(n); 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' } */ export 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 */ export function _shuffle(array, opt = {}) { const a = opt.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. */ export 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). */ export function _lastOrUndefined(array) { return array[array.length - 1]; } /** * Returns the first item of non-empty array. * Throws if array is empty. */ export function _first(array) { if (!array.length) throw new Error('_first called on empty array'); return array[0]; } export 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). */ export function _min(array) { const min = _minOrUndefined(array); _assert(min !== undefined, '_min called on empty array'); return min; } export 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). */ export function _max(array) { const max = _maxOrUndefined(array); _assert(max !== undefined, '_max called on empty array'); return max; } export function _maxBy(array, mapper) { const max = _maxByOrUndefined(array, mapper); _assert(max !== undefined, '_maxBy returned undefined'); return max; } export function _minBy(array, mapper) { const min = _minByOrUndefined(array, mapper); _assert(min !== undefined, '_minBy returned undefined'); return min; } // todo: looks like it _maxByOrUndefined/_minByOrUndefined can be DRYer export function _maxByOrUndefined(array, mapper) { if (!array.length) return; let maxItem; let max; for (let i = 0; i < array.length; i++) { const item = array[i]; const v = mapper(item); if (v !== undefined && (max === undefined || v > max)) { maxItem = item; max = v; } } return maxItem; } export function _minByOrUndefined(array, mapper) { if (!array.length) return; let minItem; let min; for (let i = 0; i < array.length; i++) { const item = array[i]; const v = mapper(item); if (v !== undefined && (min === undefined || v < min)) { minItem = item; min = v; } } return minItem; } export 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; }