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
+1
View File
@@ -0,0 +1 @@
export * from './isEqual';
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { isEqual } from '.';
describe('isEqual', () => {
it('compare primitives', () => {
expect(isEqual(1, 1)).toBe(true);
expect(isEqual('a', 'a')).toBe(true);
expect(isEqual(1, 2)).toBe(false);
expect(isEqual(1, '1')).toBe(false);
expect(isEqual(null, null)).toBe(true);
expect(isEqual(null, undefined)).toBe(false);
});
it('treat NaN as equal to NaN', () => {
expect(isEqual(Number.NaN, Number.NaN)).toBe(true);
expect(isEqual(Number.NaN, 1)).toBe(false);
});
it('compare arrays deeply', () => {
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
expect(isEqual([1, [2, 3]], [1, [2, 3]])).toBe(true);
expect(isEqual([1, 2], [1, 2, 3])).toBe(false);
expect(isEqual([1, { a: 2 }], [1, { a: 3 }])).toBe(false);
});
it('compare plain objects deeply', () => {
expect(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true);
expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
expect(isEqual({ a: 1 }, { a: 2 })).toBe(false);
});
it('compare dates and regexps', () => {
expect(isEqual(new Date(0), new Date(0))).toBe(true);
expect(isEqual(new Date(0), new Date(1))).toBe(false);
expect(isEqual(/a/gi, /a/gi)).toBe(true);
expect(isEqual(/a/g, /a/i)).toBe(false);
});
it('compare Map and Set', () => {
expect(isEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(true);
expect(isEqual(new Map([['a', 1]]), new Map([['a', 2]]))).toBe(false);
expect(isEqual(new Set([1, 2]), new Set([1, 2]))).toBe(true);
expect(isEqual(new Set([1, 2]), new Set([1, 3]))).toBe(false);
});
it('distinguish arrays from objects', () => {
expect(isEqual([], {})).toBe(false);
});
it('handle circular references', () => {
const a: any = { name: 'a' };
a.self = a;
const b: any = { name: 'a' };
b.self = b;
expect(isEqual(a, b)).toBe(true);
});
});
@@ -0,0 +1,95 @@
function equals(a: any, b: any, seen: WeakMap<object, unknown>): boolean {
if (a === b)
return true;
// NaN is the only value not equal to itself; treat NaN === NaN.
if (typeof a === 'number' && typeof b === 'number')
return Number.isNaN(a) && Number.isNaN(b);
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object')
return false;
// Cycle guard: if we have already paired `a`, it must pair with the same `b`.
const paired = seen.get(a);
if (paired !== undefined)
return paired === b;
seen.set(a, b);
if (a instanceof Date || b instanceof Date)
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
if (a instanceof RegExp || b instanceof RegExp)
return a instanceof RegExp && b instanceof RegExp && a.source === b.source && a.flags === b.flags;
const aIsArray = Array.isArray(a);
const bIsArray = Array.isArray(b);
if (aIsArray || bIsArray) {
if (!aIsArray || !bIsArray || a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (!equals(a[i], b[i], seen))
return false;
}
return true;
}
if (a instanceof Map || b instanceof Map) {
if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size)
return false;
for (const [key, value] of a) {
if (!b.has(key) || !equals(value, b.get(key), seen))
return false;
}
return true;
}
if (a instanceof Set || b instanceof Set) {
if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size)
return false;
for (const value of a) {
if (!b.has(value))
return false;
}
return true;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length)
return false;
for (const key of aKeys) {
if (!Object.prototype.hasOwnProperty.call(b, key) || !equals(a[key], b[key], seen))
return false;
}
return true;
}
/**
* @name isEqual
* @category Comparators
* @description Deep structural equality between two values. Handles primitives
* (NaN-aware), `Date`, `RegExp`, arrays, `Map`, `Set`, and plain objects, and is
* safe against circular references. `Set` membership is compared shallowly.
*
* @param {unknown} a - The first value
* @param {unknown} b - The second value
* @returns {boolean} `true` if the values are deeply equal
*
* @example
* isEqual({ a: [1, 2] }, { a: [1, 2] }); // true
* isEqual([1, { b: 2 }], [1, { b: 3 }]); // false
* isEqual(Number.NaN, Number.NaN); // true
*
* @since 0.0.10
*/
export function isEqual(a: unknown, b: unknown): boolean {
return equals(a, b, new WeakMap());
}