feat(stdlib): add immutable array ops, deep set, and isEqual comparator

Adds arrays/{move,insert,swap,remove}, collections/set, and comparators/isEqual (NaN/Date/RegExp/Map/Set/cycle-safe), wired into the barrels.
This commit is contained in:
2026-06-08 15:50:59 +07:00
parent 4678a372b1
commit 74fbd0c005
22 changed files with 479 additions and 7 deletions
+4
View File
@@ -1,10 +1,14 @@
export * from './cluster';
export * from './first';
export * from './groupBy';
export * from './insert';
export * from './last';
export * from './move';
export * from './partition';
export * from './range';
export * from './remove';
export * from './sum';
export * from './swap';
export * from './toArray';
export * from './unique';
export * from './zip';
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { insert } from '.';
describe('insert', () => {
it('insert a single item', () => {
expect(insert(['a', 'c'], 1, 'b')).toEqual(['a', 'b', 'c']);
});
it('insert multiple items', () => {
expect(insert(['a', 'd'], 1, 'b', 'c')).toEqual(['a', 'b', 'c', 'd']);
});
it('prepend at index 0', () => {
expect(insert(['b', 'c'], 0, 'a')).toEqual(['a', 'b', 'c']);
});
it('append when the index is too large', () => {
expect(insert(['a'], 99, 'b', 'c')).toEqual(['a', 'b', 'c']);
});
it('clamp a negative index to 0', () => {
expect(insert(['b'], -5, 'a')).toEqual(['a', 'b']);
});
it('never mutate the source', () => {
const source = ['a', 'b'];
insert(source, 1, 'x');
expect(source).toEqual(['a', 'b']);
});
});
+24
View File
@@ -0,0 +1,24 @@
/**
* @name insert
* @category Arrays
* @description Return a new array with `items` inserted at `index`. The index is
* clamped into `[0, length]`, so a too-large index appends. Never mutates.
*
* @param {readonly T[]} array - The source array
* @param {number} index - Position to insert at
* @param {...T} items - Items to insert
* @returns {T[]} A new array with the items inserted
*
* @example
* insert(['a', 'c'], 1, 'b'); // ['a', 'b', 'c']
* insert(['a'], 99, 'b', 'c'); // ['a', 'b', 'c']
*
* @since 0.0.10
*/
export function insert<T>(array: readonly T[], index: number, ...items: T[]): T[] {
const result = array.slice();
const target = Math.max(0, Math.min(index, result.length));
result.splice(target, 0, ...items);
return result;
}
+29
View File
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { move } from '.';
describe('move', () => {
it('move an item forward', () => {
expect(move(['a', 'b', 'c'], 0, 2)).toEqual(['b', 'c', 'a']);
});
it('move an item backward', () => {
expect(move(['a', 'b', 'c'], 2, 0)).toEqual(['c', 'a', 'b']);
});
it('clamp the target index', () => {
expect(move(['a', 'b', 'c'], 0, 99)).toEqual(['b', 'c', 'a']);
});
it('return a copy unchanged for an out-of-range source', () => {
const source = ['a', 'b'];
const result = move(source, 5, 0);
expect(result).toEqual(['a', 'b']);
expect(result).not.toBe(source);
});
it('never mutate the source', () => {
const source = ['a', 'b', 'c'];
move(source, 0, 2);
expect(source).toEqual(['a', 'b', 'c']);
});
});
+28
View File
@@ -0,0 +1,28 @@
/**
* @name move
* @category Arrays
* @description Return a new array with the item at `from` moved to `to`. Out-of-range
* `from` returns a shallow copy unchanged; `to` is clamped into range. Never mutates.
*
* @param {readonly T[]} array - The source array
* @param {number} from - Index to move from
* @param {number} to - Index to move to
* @returns {T[]} A new array with the item moved
*
* @example
* move(['a', 'b', 'c'], 0, 2); // ['b', 'c', 'a']
*
* @since 0.0.10
*/
export function move<T>(array: readonly T[], from: number, to: number): T[] {
const result = array.slice();
if (from < 0 || from >= result.length)
return result;
const item = result.splice(from, 1)[0] as T;
const target = Math.max(0, Math.min(to, result.length));
result.splice(target, 0, item);
return result;
}
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { remove } from '.';
describe('remove', () => {
it('remove an item by index', () => {
expect(remove(['a', 'b', 'c'], 1)).toEqual(['a', 'c']);
});
it('remove the first and last items', () => {
expect(remove(['a', 'b', 'c'], 0)).toEqual(['b', 'c']);
expect(remove(['a', 'b', 'c'], 2)).toEqual(['a', 'b']);
});
it('return a copy unchanged for an out-of-range index', () => {
const source = ['a', 'b'];
const result = remove(source, 9);
expect(result).toEqual(['a', 'b']);
expect(result).not.toBe(source);
});
it('never mutate the source', () => {
const source = ['a', 'b', 'c'];
remove(source, 1);
expect(source).toEqual(['a', 'b', 'c']);
});
});
+25
View File
@@ -0,0 +1,25 @@
/**
* @name remove
* @category Arrays
* @description Return a new array with the item at `index` removed. Returns a shallow
* copy unchanged when the index is out of range. Never mutates.
*
* @param {readonly T[]} array - The source array
* @param {number} index - Index of the item to remove
* @returns {T[]} A new array without the removed item
*
* @example
* remove(['a', 'b', 'c'], 1); // ['a', 'c']
*
* @since 0.0.10
*/
export function remove<T>(array: readonly T[], index: number): T[] {
const result = array.slice();
if (index < 0 || index >= result.length)
return result;
result.splice(index, 1);
return result;
}
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { swap } from '.';
describe('swap', () => {
it('swap two items', () => {
expect(swap(['a', 'b', 'c'], 0, 2)).toEqual(['c', 'b', 'a']);
});
it('return a copy unchanged when indices are equal', () => {
const source = ['a', 'b'];
const result = swap(source, 1, 1);
expect(result).toEqual(['a', 'b']);
expect(result).not.toBe(source);
});
it('return a copy unchanged for out-of-range indices', () => {
expect(swap(['a', 'b'], 0, 9)).toEqual(['a', 'b']);
expect(swap(['a', 'b'], -1, 1)).toEqual(['a', 'b']);
});
it('never mutate the source', () => {
const source = ['a', 'b', 'c'];
swap(source, 0, 2);
expect(source).toEqual(['a', 'b', 'c']);
});
});
+29
View File
@@ -0,0 +1,29 @@
/**
* @name swap
* @category Arrays
* @description Return a new array with the items at indices `a` and `b` swapped.
* Returns a shallow copy unchanged when either index is out of range or equal.
* Never mutates.
*
* @param {readonly T[]} array - The source array
* @param {number} a - First index
* @param {number} b - Second index
* @returns {T[]} A new array with the two items swapped
*
* @example
* swap(['a', 'b', 'c'], 0, 2); // ['c', 'b', 'a']
*
* @since 0.0.10
*/
export function swap<T>(array: readonly T[], a: number, b: number): T[] {
const result = array.slice();
if (a < 0 || b < 0 || a >= result.length || b >= result.length || a === b)
return result;
const temp = result[a] as T;
result[a] = result[b] as T;
result[b] = temp;
return result;
}