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:
@@ -1,10 +1,14 @@
|
|||||||
export * from './cluster';
|
export * from './cluster';
|
||||||
export * from './first';
|
export * from './first';
|
||||||
export * from './groupBy';
|
export * from './groupBy';
|
||||||
|
export * from './insert';
|
||||||
export * from './last';
|
export * from './last';
|
||||||
|
export * from './move';
|
||||||
export * from './partition';
|
export * from './partition';
|
||||||
export * from './range';
|
export * from './range';
|
||||||
|
export * from './remove';
|
||||||
export * from './sum';
|
export * from './sum';
|
||||||
|
export * from './swap';
|
||||||
export * from './toArray';
|
export * from './toArray';
|
||||||
export * from './unique';
|
export * from './unique';
|
||||||
export * from './zip';
|
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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './get';
|
export * from './get';
|
||||||
|
export * from './set';
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { set } from '.';
|
||||||
|
|
||||||
|
describe('set', () => {
|
||||||
|
it('set a top-level property', () => {
|
||||||
|
expect(set({ name: 'John' }, 'name', 'Jane')).toEqual({ name: 'Jane' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set a nested object property', () => {
|
||||||
|
expect(set({ user: { name: 'John' } }, 'user.name', 'Jane')).toEqual({ user: { name: 'Jane' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set through an array index', () => {
|
||||||
|
expect(set({ items: [{ id: 1 }, { id: 2 }] }, 'items.1.id', 9)).toEqual({ items: [{ id: 1 }, { id: 9 }] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create missing object intermediates', () => {
|
||||||
|
expect(set({}, 'a.b.c', 42)).toEqual({ a: { b: { c: 42 } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create an array when the next segment is numeric', () => {
|
||||||
|
const result = set({} as Record<string, unknown>, 'items.0.id', 1);
|
||||||
|
expect(Array.isArray((result as any).items)).toBe(true);
|
||||||
|
expect(result).toEqual({ items: [{ id: 1 }] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrite a non-object intermediate', () => {
|
||||||
|
expect(set({ a: 1 } as Record<string, unknown>, 'a.b', 2)).toEqual({ a: { b: 2 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mutate and return the same reference', () => {
|
||||||
|
const source = { a: 1 };
|
||||||
|
expect(set(source, 'a', 2)).toBe(source);
|
||||||
|
expect(source.a).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserve falsy values', () => {
|
||||||
|
expect(set({}, 'count', 0)).toEqual({ count: 0 });
|
||||||
|
expect(set({}, 'flag', false)).toEqual({ flag: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return the object unchanged for an empty path', () => {
|
||||||
|
const source = { a: 1 };
|
||||||
|
expect(set(source, '', 2)).toBe(source);
|
||||||
|
expect(source).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Collection } from '../../types';
|
||||||
|
|
||||||
|
// Hoisted so it is compiled once rather than re-created on each `set` call.
|
||||||
|
const NUMERIC_SEGMENT = /^\d+$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name set
|
||||||
|
* @category Collections
|
||||||
|
* @description Write a deeply nested value into a collection by a dot-separated path,
|
||||||
|
* creating any missing intermediate containers along the way. A numeric next segment
|
||||||
|
* creates an array, otherwise an object. Mutates and returns the original collection.
|
||||||
|
*
|
||||||
|
* @param {Collection} obj - The target object or array (mutated in place)
|
||||||
|
* @param {string} path - Dot-separated path, e.g. `'user.addresses.0.street'`
|
||||||
|
* @param {unknown} value - The value to assign at the path
|
||||||
|
* @returns {Collection} The same `obj`, for chaining
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* set({ user: { name: 'John' } }, 'user.name', 'Jane'); // { user: { name: 'Jane' } }
|
||||||
|
* set({}, 'items.0.id', 1); // { items: [{ id: 1 }] }
|
||||||
|
*
|
||||||
|
* @since 0.0.10
|
||||||
|
*/
|
||||||
|
export function set<O extends Collection>(obj: O, path: string, value: unknown): O {
|
||||||
|
if (path === '')
|
||||||
|
return obj;
|
||||||
|
|
||||||
|
const keys = path.split('.');
|
||||||
|
const lastKey = keys[keys.length - 1]!;
|
||||||
|
let current: any = obj;
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const key = keys[i]!;
|
||||||
|
const next = current[key];
|
||||||
|
|
||||||
|
// Create the missing intermediate: an array when the next segment is a numeric
|
||||||
|
// index, otherwise a plain object.
|
||||||
|
if (next === null || typeof next !== 'object')
|
||||||
|
current[key] = NUMERIC_SEGMENT.test(keys[i + 1]!) ? [] : {};
|
||||||
|
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
current[lastKey] = value;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ export * from './arrays';
|
|||||||
export * from './async';
|
export * from './async';
|
||||||
export * from './bits';
|
export * from './bits';
|
||||||
export * from './collections';
|
export * from './collections';
|
||||||
|
export * from './comparators';
|
||||||
export * from './functions';
|
export * from './functions';
|
||||||
export * from './math';
|
export * from './math';
|
||||||
export * from './objects';
|
export * from './objects';
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function levenshteinDistance(left: string, right: string): number {
|
|||||||
|
|
||||||
for (let j = 1; j <= innerLength; j++) {
|
for (let j = 1; j <= innerLength; j++) {
|
||||||
const cost = outerChar === inner[j - 1] ? 0 : 1;
|
const cost = outerChar === inner[j - 1] ? 0 : 1;
|
||||||
current[j]! = Math.min(
|
current[j] = Math.min(
|
||||||
prev[j]! + 1, // insertion
|
prev[j]! + 1, // insertion
|
||||||
current[j - 1]! + 1, // deletion
|
current[j - 1]! + 1, // deletion
|
||||||
prev[j - 1]! + cost, // substitution
|
prev[j - 1]! + cost, // substitution
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type TemplateFallback = string | ((key: string) => string);
|
|||||||
/**
|
/**
|
||||||
* Type of a template string with placeholders.
|
* Type of a template string with placeholders.
|
||||||
*/
|
*/
|
||||||
const TEMPLATE_PLACEHOLDER = /\{\s*([^{}]+?)\s*\}/gm;
|
const TEMPLATE_PLACEHOLDER = /\{([^{}]+)\}/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the placeholder syntax from a template string.
|
* Removes the placeholder syntax from a template string.
|
||||||
@@ -82,8 +82,8 @@ export function templateObject<
|
|||||||
T extends string,
|
T extends string,
|
||||||
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue> & Collection,
|
A extends GenerateTypes<ExtractPlaceholders<T>, TemplateValue> & Collection,
|
||||||
>(template: T, args: A, fallback?: TemplateFallback): string {
|
>(template: T, args: A, fallback?: TemplateFallback): string {
|
||||||
return template.replace(TEMPLATE_PLACEHOLDER, (_match, key: string) => {
|
return template.replaceAll(TEMPLATE_PLACEHOLDER, (_match, key: string) => {
|
||||||
const value = get(args, key);
|
const value = get(args, key.trim());
|
||||||
|
|
||||||
if (value !== null && value !== undefined)
|
if (value !== null && value !== undefined)
|
||||||
return String(value);
|
return String(value);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe('complex', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('true for class instances and null-prototype objects', () => {
|
it('true for class instances and null-prototype objects', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class -- fixture for the instance check
|
||||||
class Foo {}
|
class Foo {}
|
||||||
|
|
||||||
expect(isObject(new Foo())).toBe(true);
|
expect(isObject(new Foo())).toBe(true);
|
||||||
@@ -48,6 +49,7 @@ describe('complex', () => {
|
|||||||
describe('isRegExp', () => {
|
describe('isRegExp', () => {
|
||||||
it('true if the value is a regexp', () => {
|
it('true if the value is a regexp', () => {
|
||||||
expect(isRegExp(/test/)).toBe(true);
|
expect(isRegExp(/test/)).toBe(true);
|
||||||
|
// eslint-disable-next-line prefer-regex-literals -- intentionally testing the constructor form
|
||||||
expect(isRegExp(new RegExp('test'))).toBe(true);
|
expect(isRegExp(new RegExp('test'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('primitives', () => {
|
|||||||
|
|
||||||
describe('isSymbol', () => {
|
describe('isSymbol', () => {
|
||||||
it('true if the value is a symbol', () => {
|
it('true if the value is a symbol', () => {
|
||||||
expect(isSymbol(Symbol())).toBe(true);
|
expect(isSymbol(Symbol('test'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('false if the value is not a symbol', () => {
|
it('false if the value is not a symbol', () => {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const isSymbol = (value: any): value is symbol => typeof value === 'symbo
|
|||||||
*
|
*
|
||||||
* @since 0.0.2
|
* @since 0.0.2
|
||||||
*/
|
*/
|
||||||
export const isUndefined = (value: any): value is undefined => typeof value === 'undefined';
|
export const isUndefined = (value: any): value is undefined => value === undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name isNull
|
* @name isNull
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe('string', () => {
|
|||||||
it('should be a string', () => {
|
it('should be a string', () => {
|
||||||
expectTypeOf(Number(1)).toExtend<Stringable>();
|
expectTypeOf(Number(1)).toExtend<Stringable>();
|
||||||
expectTypeOf(String(1)).toExtend<Stringable>();
|
expectTypeOf(String(1)).toExtend<Stringable>();
|
||||||
expectTypeOf(Symbol()).toExtend<Stringable>();
|
expectTypeOf(Symbol('test')).toExtend<Stringable>();
|
||||||
expectTypeOf([1]).toExtend<Stringable>();
|
expectTypeOf([1]).toExtend<Stringable>();
|
||||||
expectTypeOf(new Object()).toExtend<Stringable>();
|
expectTypeOf(new Object()).toExtend<Stringable>();
|
||||||
expectTypeOf(new Date()).toExtend<Stringable>();
|
expectTypeOf(new Date()).toExtend<Stringable>();
|
||||||
|
|||||||
Reference in New Issue
Block a user