src/array/array.util.ts
import { _isNotNullish } from '../is.util'
import {
AbortablePredicate,
END,
FalsyValue,
Mapper,
Predicate,
SortDirection,
StringMap,
} from '../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
*/
export function _chunk<T>(array: readonly T[], size = 1): T[][] {
return array.reduce((arr, item, idx) => {
return idx % size === 0 ? [...arr, [item]] : [...arr.slice(0, -1), [...arr.slice(-1)[0]!, item]]
}, [] as T[][])
}
/**
* Removes duplicates from given array.
*/
export function _uniq<T>(a: readonly T[]): T[] {
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<T>(a: T[], ...items: T[]): T[] {
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<T>(a: T[], mapper: Mapper<T, any>, ...items: T[]): T[] {
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
*/
export function _uniqBy<T>(arr: readonly T[], mapper: Mapper<T, any>): T[] {
return [
...arr
.reduce((map, item, index) => {
const key = item === null || item === undefined ? item : mapper(item, index)
if (!map.has(key)) map.set(key, item)
return map
}, new 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<T>(items: readonly T[], mapper: Mapper<T, any>): StringMap<T> {
return Object.fromEntries(
items.map((item, i) => [mapper(item, i), item]).filter(([k]) => k !== undefined) as [any, T][],
)
}
/**
* Map an array of items by a key, that is calculated by a Mapper.
*/
export function _mapBy<ITEM, KEY>(
items: readonly ITEM[],
mapper: Mapper<ITEM, KEY>,
): Map<KEY, ITEM> {
return new Map(
items.map((item, i) => [mapper(item, i), item]).filter(([k]) => k !== undefined) as [
KEY,
ITEM,
][],
)
}
/**
* 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<T>(items: readonly T[], mapper: Mapper<T, any>): StringMap<T[]> {
return items.reduce(
(map, item, index) => {
const res = mapper(item, index)
if (res !== undefined) {
map[res] = [...(map[res] || []), item]
}
return map
},
{} as StringMap<T[]>,
)
}
/**
* _sortBy([{age: 20}, {age: 10}], 'age')
* // => [{age: 10}, {age: 20}]
*
* Same:
* _sortBy([{age: 20}, {age: 10}], o => o.age)
*/
export function _sortBy<T>(
items: T[],
mapper: Mapper<T, any>,
mutate = false,
dir: SortDirection = 'asc',
): T[] {
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.
*/
export function _sortDescBy<T>(items: T[], mapper: Mapper<T, any>, mutate = false): T[] {
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.
*/
export function _find<T>(items: readonly T[], predicate: AbortablePredicate<T>): T | undefined {
for (const [i, item] of items.entries()) {
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<T>(items: readonly T[], predicate: AbortablePredicate<T>): T | undefined {
return _find(items.slice().reverse(), predicate)
}
export function _takeWhile<T>(items: readonly T[], predicate: Predicate<T>): T[] {
let proceed = true
return items.filter((v, index) => (proceed &&= predicate(v, index)))
}
export function _takeRightWhile<T>(items: readonly T[], predicate: Predicate<T>): T[] {
let proceed = true
return [...items].reverse().filter((v, index) => (proceed &&= predicate(v, index)))
}
export function _dropWhile<T>(items: readonly T[], predicate: Predicate<T>): T[] {
let proceed = false
return items.filter((v, index) => (proceed ||= !predicate(v, index)))
}
export function _dropRightWhile<T>(items: readonly T[], predicate: Predicate<T>): T[] {
let proceed = false
return [...items]
.reverse()
.filter((v, index) => (proceed ||= !predicate(v, index)))
.reverse()
}
/**
* 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<T>(
items: Iterable<T>,
predicate: AbortablePredicate<T>,
limit?: number,
): number {
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<T>(items: Iterable<T>, mapper: Mapper<T, any>): StringMap<number> {
const map: StringMap<number> = {}
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]
*/
export function _intersection<T>(a1: T[], a2: T[] | Set<T>): T[] {
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<T>(a1: T[], a2: T[] | Set<T>): boolean {
const a2set = a2 instanceof Set ? a2 : new Set(a2)
return a1.some(v => a2set.has(v))
}
/**
* @example
* _difference([2, 1], [2, 3])
* // [1]
*/
export function _difference<T>(source: T[], ...diffs: T[][]): T[] {
return diffs.reduce((a, b) => a.filter(c => !b.includes(c)), source)
}
/**
* Returns the sum of items, or 0 for empty array.
*/
export function _sum(items: Iterable<number>): number {
let sum = 0
for (const n of items) {
sum += n
}
return sum
}
export function _sumBy<T>(items: Iterable<T>, mapper: Mapper<T, number | undefined>): number {
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 += 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<T, V>(
array: Iterable<T>,
mapper: (item: T) => [key: any, value: V] | FalsyValue,
): StringMap<V> {
const m: StringMap<V> = {}
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<T>(array: T[], mutate = false): T[] {
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.
*/
export function _last<T>(array: readonly T[]): T {
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<T>(array: readonly T[]): T | undefined {
return array[array.length - 1]
}
/**
* Returns the first item of non-empty array.
* Throws if array is empty.
*/
export function _first<T>(array: readonly T[]): T {
if (!array.length) throw new Error('_first called on empty array')
return array[0]!
}
export function _minOrUndefined<T>(array: readonly T[]): NonNullable<T> | undefined {
const a = array.filter(_isNotNullish)
if (!a.length) return
return a.reduce((min, item) => (min <= item ? min : item))
}
/**
* Filters out nullish values (undefined and null).
*/
export function _min<T>(array: readonly T[]): NonNullable<T> {
const a = array.filter(_isNotNullish)
if (!a.length) throw new Error('_min called on empty array')
return a.reduce((min, item) => (min <= item ? min : item))
}
export function _maxOrUndefined<T>(array: readonly T[]): NonNullable<T> | undefined {
const a = array.filter(_isNotNullish)
if (!a.length) return
return a.reduce((max, item) => (max >= item ? max : item))
}
/**
* Filters out nullish values (undefined and null).
*/
export function _max<T>(array: readonly T[]): NonNullable<T> {
const a = array.filter(_isNotNullish)
if (!a.length) throw new Error('_max called on empty array')
return a.reduce((max, item) => (max >= item ? max : item))
}
export function _maxBy<T>(array: readonly T[], mapper: Mapper<T, number | string | undefined>): T {
const max = _maxByOrUndefined(array, mapper)
if (max === undefined) throw new Error(`_maxBy returned undefined`)
return max
}
export function _minBy<T>(array: readonly T[], mapper: Mapper<T, number | string | undefined>): T {
const min = _minByOrUndefined(array, mapper)
if (min === undefined) throw new Error(`_minBy returned undefined`)
return min
}
// todo: looks like it _maxByOrUndefined/_minByOrUndefined can be DRYer
export function _maxByOrUndefined<T>(
array: readonly T[],
mapper: Mapper<T, number | string | undefined>,
): T | undefined {
if (!array.length) return
let maxItem: T | undefined
let max: number | string | undefined
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
}
export function _minByOrUndefined<T>(
array: readonly T[],
mapper: Mapper<T, number | string | undefined>,
): T | undefined {
if (!array.length) return
let minItem: T | undefined
let min: number | string | undefined
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
}
export function _zip<T1, T2>(array1: readonly T1[], array2: readonly T2[]): [T1, T2][] {
const len = Math.min(array1.length, array2.length)
const res: [T1, T2][] = []
for (let i = 0; i < len; i++) {
res.push([array1[i]!, array2[i]!])
}
return res
}