feat(stdlib): new modules + eslint/tsconfig migration
- Add array/async/etc. modules and type tests; migrate to eslint flat config and composite tsconfig (vitest typecheck enabled). - Fix PubSub.emit to snapshot listeners before iterating (stable EventEmitter semantics; avoids invoking listeners added during the same emit).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { cluster } from '.';
|
||||
|
||||
describe('cluster', () => {
|
||||
@@ -37,4 +37,13 @@ describe('cluster', () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('not mutate the input and produce copied sub-arrays', () => {
|
||||
const input = [1, 2, 3, 4];
|
||||
const result = cluster(input, 2);
|
||||
|
||||
result[0]!.push(99);
|
||||
|
||||
expect(input).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { first } from '.';
|
||||
|
||||
describe('first', () => {
|
||||
@@ -20,4 +20,11 @@ describe('first', () => {
|
||||
expect(first([1, 2, 3], 42)).toBe(1);
|
||||
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
|
||||
});
|
||||
|
||||
it('preserve a present null/undefined/falsy first element (not the default)', () => {
|
||||
expect(first([null as number | null], 42)).toBeNull();
|
||||
expect(first([undefined, 2], 42)).toBeUndefined();
|
||||
expect(first([0], 99)).toBe(0);
|
||||
expect(first([''], 'x')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function first<Value>(arr: Value[], defaultValue?: Value) {
|
||||
return arr[0] ?? defaultValue;
|
||||
// Branch on length, not nullishness, so a present null/undefined first element is preserved.
|
||||
return arr.length > 0 ? arr[0]! : defaultValue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import { groupBy } from '.';
|
||||
|
||||
describe('groupBy', () => {
|
||||
it('keys the record by the union returned by the key function', () => {
|
||||
const result = groupBy([1, 2, 3], n => (n % 2 === 0 ? 'even' : 'odd'));
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Record<'even' | 'odd', number[]>>();
|
||||
});
|
||||
|
||||
it('preserves the element type in the grouped arrays', () => {
|
||||
const result = groupBy([{ id: 1 }], item => item.id);
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Record<number, Array<{ id: number }>>>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { groupBy } from '.';
|
||||
|
||||
describe('groupBy', () => {
|
||||
it('group by a string key', () => {
|
||||
const result = groupBy([1, 2, 3, 4], n => (n % 2 === 0 ? 'even' : 'odd'));
|
||||
|
||||
expect(result).toEqual({ odd: [1, 3], even: [2, 4] });
|
||||
});
|
||||
|
||||
it('group objects by a property', () => {
|
||||
const input = [
|
||||
{ type: 'a', v: 1 },
|
||||
{ type: 'b', v: 2 },
|
||||
{ type: 'a', v: 3 },
|
||||
];
|
||||
|
||||
expect(groupBy(input, item => item.type)).toEqual({
|
||||
a: [{ type: 'a', v: 1 }, { type: 'a', v: 3 }],
|
||||
b: [{ type: 'b', v: 2 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('pass the index to the key function', () => {
|
||||
const result = groupBy(['a', 'b', 'c', 'd'], (_, i) => i % 2);
|
||||
|
||||
expect(result).toEqual({ 0: ['a', 'c'], 1: ['b', 'd'] });
|
||||
});
|
||||
|
||||
it('return an empty object for an empty array', () => {
|
||||
expect(groupBy([], () => 'x')).toEqual({});
|
||||
});
|
||||
|
||||
it('push elements by reference (no cloning)', () => {
|
||||
const item = { id: 1 };
|
||||
const result = groupBy([item], x => x.id);
|
||||
|
||||
expect(result[1]![0]).toBe(item);
|
||||
});
|
||||
|
||||
it('handle __proto__ and other Object.prototype keys as ordinary groups', () => {
|
||||
const proto = groupBy(['a', 'b'], (): string => '__proto__');
|
||||
expect(proto.__proto__).toEqual(['a', 'b']);
|
||||
|
||||
const ctor = groupBy(['x'], (): string => 'constructor');
|
||||
expect(ctor.constructor).toEqual(['x']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @name groupBy
|
||||
* @category Arrays
|
||||
* @description Groups the elements of an array by the key returned by `getKey`
|
||||
*
|
||||
* @param {Value[]} array - The array to group
|
||||
* @param {(item: Value, index: number) => Key} getKey - Maps an element to its group key
|
||||
* @returns {Record<Key, Value[]>} An object of arrays, keyed by group
|
||||
*
|
||||
* @example
|
||||
* groupBy([1, 2, 3, 4], n => (n % 2 === 0 ? 'even' : 'odd'))
|
||||
* // => { odd: [1, 3], even: [2, 4] }
|
||||
*
|
||||
* @example
|
||||
* groupBy([{ type: 'a', v: 1 }, { type: 'b', v: 2 }, { type: 'a', v: 3 }], item => item.type)
|
||||
* // => { a: [{ type: 'a', v: 1 }, { type: 'a', v: 3 }], b: [{ type: 'b', v: 2 }] }
|
||||
*
|
||||
* @since 0.0.10
|
||||
*/
|
||||
export function groupBy<Value, Key extends PropertyKey>(
|
||||
array: Value[],
|
||||
getKey: (item: Value, index: number) => Key,
|
||||
): Record<Key, Value[]> {
|
||||
// Null-prototype object so keys like '__proto__'/'constructor' become ordinary own keys
|
||||
// instead of colliding with Object.prototype (which would throw on .push or pollute).
|
||||
const result = Object.create(null) as Record<Key, Value[]>;
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i]!;
|
||||
const key = getKey(item, i);
|
||||
|
||||
(result[key] ??= []).push(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
export * from './cluster';
|
||||
export * from './first';
|
||||
export * from './groupBy';
|
||||
export * from './last';
|
||||
export * from './partition';
|
||||
export * from './range';
|
||||
export * from './sum';
|
||||
export * from './toArray';
|
||||
export * from './unique';
|
||||
export * from './zip';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { last } from '.';
|
||||
|
||||
describe('last', () => {
|
||||
@@ -20,4 +20,10 @@ describe('last', () => {
|
||||
expect(last([1, 2, 3], 42)).toBe(3);
|
||||
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
|
||||
});
|
||||
|
||||
it('preserve a present null/undefined/falsy last element (not the default)', () => {
|
||||
expect(last([1, null as number | null], 42)).toBeNull();
|
||||
expect(last([1, undefined], 42)).toBeUndefined();
|
||||
expect(last([0], 99)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function last<Value>(arr: Value[], defaultValue?: Value) {
|
||||
return arr[arr.length - 1] ?? defaultValue;
|
||||
// Branch on length, not nullishness, so a present null/undefined last element is preserved.
|
||||
return arr.length > 0 ? arr[arr.length - 1]! : defaultValue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import { partition } from '.';
|
||||
|
||||
describe('partition', () => {
|
||||
it('returns a tuple of two arrays of the element type', () => {
|
||||
const result = partition([1, 2, 3], n => n > 1);
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<[number[], number[]]>();
|
||||
});
|
||||
|
||||
it('narrows both partitions with a type guard', () => {
|
||||
const mixed: Array<string | number> = ['a', 1];
|
||||
const result = partition(mixed, (v): v is string => typeof v === 'string');
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<[string[], number[]]>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { partition } from '.';
|
||||
|
||||
describe('partition', () => {
|
||||
it('split by a predicate into [matching, rest]', () => {
|
||||
expect(partition([1, 2, 3, 4], n => n % 2 === 0)).toEqual([[2, 4], [1, 3]]);
|
||||
});
|
||||
|
||||
it('preserve order within each partition', () => {
|
||||
expect(partition([5, 1, 4, 2, 3], n => n > 2)).toEqual([[5, 4, 3], [1, 2]]);
|
||||
});
|
||||
|
||||
it('pass the index to the predicate', () => {
|
||||
expect(partition(['a', 'b', 'c', 'd'], (_, i) => i < 2)).toEqual([['a', 'b'], ['c', 'd']]);
|
||||
});
|
||||
|
||||
it('handle all-matching and none-matching', () => {
|
||||
expect(partition([1, 2, 3], () => true)).toEqual([[1, 2, 3], []]);
|
||||
expect(partition([1, 2, 3], () => false)).toEqual([[], [1, 2, 3]]);
|
||||
});
|
||||
|
||||
it('work with a type guard', () => {
|
||||
const mixed: Array<string | number> = ['a', 1, 'b', 2];
|
||||
const [strings, numbers] = partition(mixed, (v): v is string => typeof v === 'string');
|
||||
|
||||
expect(strings).toEqual(['a', 'b']);
|
||||
expect(numbers).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @name partition
|
||||
* @category Arrays
|
||||
* @description Splits an array into two: elements that satisfy the predicate and those that do not
|
||||
*
|
||||
* @param {Value[]} array - The array to split
|
||||
* @param {(item: Value, index: number) => boolean} predicate - Decides which partition an element belongs to
|
||||
* @returns {[Value[], Value[]]} A tuple of `[matching, rest]`
|
||||
*
|
||||
* @example
|
||||
* partition([1, 2, 3, 4], n => n % 2 === 0) // => [[2, 4], [1, 3]]
|
||||
*
|
||||
* @example
|
||||
* const [strings, others] = partition(mixed, (v): v is string => typeof v === 'string');
|
||||
*
|
||||
* @since 0.0.10
|
||||
*/
|
||||
export function partition<Value, Matched extends Value>(
|
||||
array: Value[],
|
||||
predicate: (item: Value, index: number) => item is Matched,
|
||||
): [Matched[], Array<Exclude<Value, Matched>>];
|
||||
export function partition<Value>(
|
||||
array: Value[],
|
||||
predicate: (item: Value, index: number) => boolean,
|
||||
): [Value[], Value[]];
|
||||
export function partition<Value>(
|
||||
array: Value[],
|
||||
predicate: (item: Value, index: number) => boolean,
|
||||
): [Value[], Value[]] {
|
||||
const matched: Value[] = [];
|
||||
const rest: Value[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i]!;
|
||||
|
||||
if (predicate(item, i))
|
||||
matched.push(item);
|
||||
else
|
||||
rest.push(item);
|
||||
}
|
||||
|
||||
return [matched, rest];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { range } from '.';
|
||||
|
||||
describe('range', () => {
|
||||
it('generate 0..stop with a single argument', () => {
|
||||
expect(range(4)).toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('generate start..stop', () => {
|
||||
expect(range(1, 5)).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('respect a positive step', () => {
|
||||
expect(range(0, 10, 2)).toEqual([0, 2, 4, 6, 8]);
|
||||
});
|
||||
|
||||
it('support a negative step', () => {
|
||||
expect(range(5, 0, -1)).toEqual([5, 4, 3, 2, 1]);
|
||||
});
|
||||
|
||||
it('return an empty array for an empty range', () => {
|
||||
expect(range(0)).toEqual([]);
|
||||
expect(range(5, 5)).toEqual([]);
|
||||
expect(range(0, 5, -1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('return an empty array for a zero step', () => {
|
||||
expect(range(0, 5, 0)).toEqual([]);
|
||||
});
|
||||
|
||||
it('handle non-integer steps', () => {
|
||||
expect(range(0, 1, 0.25)).toEqual([0, 0.25, 0.5, 0.75]);
|
||||
});
|
||||
|
||||
it('span zero with a negative start', () => {
|
||||
expect(range(-2, 3)).toEqual([-2, -1, 0, 1, 2]);
|
||||
expect(range(-3, 3, 2)).toEqual([-3, -1, 1]);
|
||||
});
|
||||
|
||||
it('handle a non-integer step that is not exactly representable', () => {
|
||||
const result = range(0, 1, 0.1);
|
||||
|
||||
expect(result).toHaveLength(10);
|
||||
expect(result[0]).toBe(0);
|
||||
expect(result.at(-1)).toBeCloseTo(0.9, 10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @name range
|
||||
* @category Arrays
|
||||
* @description Generates an array of numbers from `start` (inclusive) to `stop` (exclusive)
|
||||
*
|
||||
* @param {number} startOrStop - The start of the range, or the stop when called with one argument
|
||||
* @param {number} [stop] - The end of the range (exclusive)
|
||||
* @param {number} [step] - The increment between values; supports negative steps. Default `1`
|
||||
* @returns {number[]} The generated range
|
||||
*
|
||||
* @example
|
||||
* range(4) // => [0, 1, 2, 3]
|
||||
*
|
||||
* @example
|
||||
* range(1, 5) // => [1, 2, 3, 4]
|
||||
*
|
||||
* @example
|
||||
* range(0, 10, 2) // => [0, 2, 4, 6, 8]
|
||||
*
|
||||
* @example
|
||||
* range(5, 0, -1) // => [5, 4, 3, 2, 1]
|
||||
*
|
||||
* @since 0.0.10
|
||||
*/
|
||||
export function range(stop: number): number[];
|
||||
export function range(start: number, stop: number, step?: number): number[];
|
||||
export function range(startOrStop: number, stop?: number, step = 1): number[] {
|
||||
let start = startOrStop;
|
||||
|
||||
if (stop === undefined) {
|
||||
start = 0;
|
||||
stop = startOrStop;
|
||||
}
|
||||
|
||||
if (step === 0)
|
||||
return [];
|
||||
|
||||
const length = Math.max(Math.ceil((stop - start) / step), 0);
|
||||
|
||||
return Array.from({ length }, (_, i) => start + i * step);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { sum } from '.';
|
||||
|
||||
describe('sum', () => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import { toArray } from '.';
|
||||
|
||||
describe('toArray', () => {
|
||||
it('wraps a single value into an array of that type', () => {
|
||||
expectTypeOf(toArray(1)).toEqualTypeOf<number[]>();
|
||||
});
|
||||
|
||||
it('returns an array input as the same element type', () => {
|
||||
expectTypeOf(toArray([1, 2, 3])).toEqualTypeOf<number[]>();
|
||||
});
|
||||
|
||||
it('returns the element array type for a nullish input', () => {
|
||||
expectTypeOf(toArray<string>(undefined)).toEqualTypeOf<string[]>();
|
||||
expectTypeOf(toArray<number>(null)).toEqualTypeOf<number[]>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { toArray } from '.';
|
||||
|
||||
describe('toArray', () => {
|
||||
it('wrap a single value into an array', () => {
|
||||
expect(toArray(1)).toEqual([1]);
|
||||
expect(toArray('a')).toEqual(['a']);
|
||||
expect(toArray(false)).toEqual([false]);
|
||||
expect(toArray(0)).toEqual([0]);
|
||||
});
|
||||
|
||||
it('return arrays as-is (same reference, no copy)', () => {
|
||||
const arr = [1, 2, 3];
|
||||
expect(toArray(arr)).toBe(arr);
|
||||
});
|
||||
|
||||
it('treat null and undefined as empty', () => {
|
||||
expect(toArray(undefined)).toEqual([]);
|
||||
expect(toArray(null)).toEqual([]);
|
||||
});
|
||||
|
||||
it('preserve empty arrays', () => {
|
||||
const empty: number[] = [];
|
||||
expect(toArray(empty)).toBe(empty);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Arrayable } from '../../types';
|
||||
|
||||
/**
|
||||
* @name toArray
|
||||
* @category Arrays
|
||||
* @description Normalize an `Arrayable<T>` value into an array. `undefined` and `null` become an empty array; a single value is wrapped; arrays are returned as-is (no copy).
|
||||
*
|
||||
* @param {Arrayable<Value> | null | undefined} value The value to normalize
|
||||
* @returns {Value[]} The value as an array
|
||||
*
|
||||
* @example
|
||||
* toArray(1) // => [1]
|
||||
*
|
||||
* @example
|
||||
* toArray([1, 2]) // => [1, 2]
|
||||
*
|
||||
* @example
|
||||
* toArray(undefined) // => []
|
||||
*
|
||||
* @since 0.0.10
|
||||
*/
|
||||
export function toArray<Value>(value: Arrayable<Value> | null | undefined): Value[] {
|
||||
if (value === null || value === undefined) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { unique } from '.';
|
||||
|
||||
describe('unique', () => {
|
||||
@@ -42,4 +42,28 @@ describe('unique', () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('keep the last value per extracted key (last-write-wins, first-seen order)', () => {
|
||||
const result = unique(
|
||||
[{ id: 1, v: 'a' }, { id: 2, v: 'b' }, { id: 1, v: 'c' }],
|
||||
item => item.id,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ id: 1, v: 'c' }, { id: 2, v: 'b' }]);
|
||||
});
|
||||
|
||||
it('return a new array and not mutate the input', () => {
|
||||
const input = [1, 2, 2, 3];
|
||||
const result = unique(input);
|
||||
|
||||
expect(result).not.toBe(input);
|
||||
expect(input).toEqual([1, 2, 2, 3]);
|
||||
});
|
||||
|
||||
it('preserve element identity for object values', () => {
|
||||
const a = { id: 1 };
|
||||
const result = unique([a], item => item.id);
|
||||
|
||||
expect(result[0]).toBe(a);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,20 +14,23 @@ export type Extractor<Value, Key extends UniqueKey> = (value: Value) => Key;
|
||||
* unique([1, 2, 3, 3, 4, 5, 5, 6]) //=> [1, 2, 3, 4, 5, 6]
|
||||
*
|
||||
* @example
|
||||
* unique([{ id: 1 }, { id: 2 }, { id: 1 }], (a, b) => a.id === b.id) //=> [{ id: 1 }, { id: 2 }]
|
||||
* unique([{ id: 1 }, { id: 2 }, { id: 1 }], value => value.id) //=> [{ id: 1 }, { id: 2 }]
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function unique<Value, Key extends UniqueKey>(
|
||||
array: Value[],
|
||||
extractor?: Extractor<Value, Key>,
|
||||
) {
|
||||
): Value[] {
|
||||
// Fast path: a plain Set is leaner than a Map storing each value as both key and value.
|
||||
if (!extractor)
|
||||
return [...new Set(array)];
|
||||
|
||||
// Last-write-wins per extracted key, preserving first-seen insertion order.
|
||||
const values = new Map<Key, Value>();
|
||||
|
||||
for (const value of array) {
|
||||
const key = extractor ? extractor(value) : value as any;
|
||||
values.set(key, value);
|
||||
}
|
||||
for (const value of array)
|
||||
values.set(extractor(value), value);
|
||||
|
||||
return Array.from(values.values());
|
||||
return [...values.values()];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import { zip } from '.';
|
||||
|
||||
describe('zip', () => {
|
||||
it('produces tuples of the element types (two arrays)', () => {
|
||||
const result = zip([1, 2], ['a', 'b']);
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Array<[number, string]>>();
|
||||
});
|
||||
|
||||
it('produces tuples of the element types (three arrays)', () => {
|
||||
const result = zip([1], ['a'], [true]);
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Array<[number, string, boolean]>>();
|
||||
});
|
||||
|
||||
it('zips a single array into singleton tuples', () => {
|
||||
const result = zip([1, 2, 3]);
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<Array<[number]>>();
|
||||
});
|
||||
|
||||
it('produces tuples for four and five arrays', () => {
|
||||
expectTypeOf(zip([1], ['a'], [true], [9])).toEqualTypeOf<Array<[number, string, boolean, number]>>();
|
||||
expectTypeOf(zip([1], ['a'], [true], [9], ['x'])).toEqualTypeOf<Array<[number, string, boolean, number, string]>>();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { zip } from '.';
|
||||
|
||||
describe('zip', () => {
|
||||
it('zip two arrays of equal length', () => {
|
||||
expect(zip([1, 2, 3], ['a', 'b', 'c'])).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]);
|
||||
});
|
||||
|
||||
it('zip three arrays', () => {
|
||||
expect(zip([1, 2], ['a', 'b'], [true, false])).toEqual([[1, 'a', true], [2, 'b', false]]);
|
||||
});
|
||||
|
||||
it('truncate to the shortest array', () => {
|
||||
expect(zip([1, 2, 3], ['a'])).toEqual([[1, 'a']]);
|
||||
expect(zip([1], ['a', 'b', 'c'])).toEqual([[1, 'a']]);
|
||||
});
|
||||
|
||||
it('zip a single array into singletons', () => {
|
||||
expect(zip([1, 2, 3])).toEqual([[1], [2], [3]]);
|
||||
});
|
||||
|
||||
it('return an empty array when an input is empty', () => {
|
||||
expect(zip([1, 2, 3], [])).toEqual([]);
|
||||
});
|
||||
|
||||
it('return an empty array with no arguments', () => {
|
||||
expect((zip as () => unknown[])()).toEqual([]);
|
||||
});
|
||||
|
||||
it('zip four and five arrays', () => {
|
||||
expect(zip([1], ['a'], [true], [9])).toEqual([[1, 'a', true, 9]]);
|
||||
expect(zip([1, 2], ['a', 'b'], [true, false], [9, 8], ['x', 'y']))
|
||||
.toEqual([[1, 'a', true, 9, 'x'], [2, 'b', false, 8, 'y']]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* @name zip
|
||||
* @category Arrays
|
||||
* @description Combines several arrays into an array of tuples, stopping at the shortest input
|
||||
*
|
||||
* @param {...Array} arrays - The arrays to zip together
|
||||
* @returns {Array} An array of tuples; its length equals the shortest input array
|
||||
*
|
||||
* @example
|
||||
* zip([1, 2, 3], ['a', 'b', 'c']) // => [[1, 'a'], [2, 'b'], [3, 'c']]
|
||||
*
|
||||
* @example
|
||||
* zip([1, 2], ['a', 'b'], [true, false]) // => [[1, 'a', true], [2, 'b', false]]
|
||||
*
|
||||
* @example
|
||||
* zip([1, 2, 3], ['a']) // => [[1, 'a']] (truncated to the shortest)
|
||||
*
|
||||
* @since 0.0.10
|
||||
*/
|
||||
export function zip<A>(a: A[]): Array<[A]>;
|
||||
export function zip<A, B>(a: A[], b: B[]): Array<[A, B]>;
|
||||
export function zip<A, B, C>(a: A[], b: B[], c: C[]): Array<[A, B, C]>;
|
||||
export function zip<A, B, C, D>(a: A[], b: B[], c: C[], d: D[]): Array<[A, B, C, D]>;
|
||||
export function zip<A, B, C, D, E>(a: A[], b: B[], c: C[], d: D[], e: E[]): Array<[A, B, C, D, E]>;
|
||||
export function zip(...arrays: any[][]): any[][] {
|
||||
if (arrays.length === 0)
|
||||
return [];
|
||||
|
||||
let length = arrays[0]!.length;
|
||||
|
||||
for (let i = 1; i < arrays.length; i++)
|
||||
length = Math.min(length, arrays[i]!.length);
|
||||
|
||||
return Array.from({ length }, (_, i) => arrays.map(array => array[i]));
|
||||
}
|
||||
Reference in New Issue
Block a user