diff --git a/src/collection.ts b/src/collection.ts index 4bb69ca..7073761 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -13,6 +13,8 @@ import { isRegExp, isTypedArray, isWeakMap, + isError, + isDOMException, } from './general' export function mergeWith, TSource extends Record>( @@ -111,7 +113,7 @@ export function cloneDeepWith(value: T, fn: (value: any) => any): T { return newConstructor(value, value.valueOf()) } - if (isWeakMap(value) || isWeakSet(value)) { + if (isWeakMap(value) || isWeakSet(value) || isError(value) || isDOMException(value)) { return {} } @@ -139,15 +141,13 @@ export function cloneDeepWith(value: T, fn: (value: any) => any): T { } if (isPlainObject(value)) { - const result: Record = Object.create(Reflect.getPrototypeOf(value)) + const result: Record = Object.create(Reflect.getPrototypeOf(value)) cache.set(value, result) - // eslint-disable-next-line no-restricted-syntax - for (const key in value) { - if (hasOwn(value, key)) { - result[key] = baseCloneDeep(value[key], cache) - } - } + const ownKeys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)] + ownKeys.forEach((key) => { + result[key] = baseCloneDeep(value[key as keyof typeof value], cache) + }) return result } diff --git a/src/general.ts b/src/general.ts index 4b04732..13229aa 100644 --- a/src/general.ts +++ b/src/general.ts @@ -20,6 +20,14 @@ export function isSymbol(val: unknown): val is symbol { return typeof val === 'symbol' } +export function isError(val: unknown): val is Error { + return toRawType(val) === 'Error' +} + +export function isDOMException(val: unknown): val is DOMException { + return toRawType(val) === 'DOMException' +} + export function isNumeric(val: unknown): val is number | string { return isNumber(val) || (isString(val) && /^[-+]?\d+$/.test(val)) } @@ -64,7 +72,20 @@ export function isArrayBuffer(val: unknown): val is ArrayBuffer { return toRawType(val) === 'ArrayBuffer' } -export function isTypedArray(val: unknown) { +export function isTypedArray( + val: unknown, +): val is + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array { return [ 'Int8Array', 'Uint8Array', @@ -146,3 +167,117 @@ export function getGlobalThis() { export function isNonEmptyArray(val: unknown): val is Array { return isArray(val) && !!val.length } + +export function isEqualWith(value: any, other: any, fn: (value: any, other: any) => any): boolean { + const cache = new WeakMap() + + function baseIsEqual(value: any, other: any, cache: WeakMap): boolean { + const customEqual = fn(value, other) + + if (customEqual === true) { + return true + } + + if (value === other) { + return true + } + + // eslint-disable-next-line no-self-compare + if (value !== value && other !== other) { + return true + } + + if (!isObject(value) || !isObject(other)) { + return value === other + } + + if (value.constructor !== other.constructor) { + return false + } + + if (isDate(value) && isDate(other)) { + return value.getTime() === other.getTime() + } + + if (isRegExp(value) && isRegExp(other)) { + return value.source === other.source && value.flags === other.flags + } + + if (isError(value) && isError(other)) { + return value.name === other.name && value.message === other.message && value.cause === other.cause + } + + if (isDOMException(value) && isDOMException(other)) { + return value.name === other.name && value.message === other.message + } + + if ((isTypedArray(value) && isTypedArray(other)) || (isDataView(value) && isDataView(other))) { + if (value.byteLength !== other.byteLength) { + return false + } + + const valueTypedArray = new Uint8Array(value.buffer) + const otherTypedArray = new Uint8Array(other.buffer) + + return valueTypedArray.every((v, i) => v === otherTypedArray[i]) + } + + if (isArrayBuffer(value) && isArrayBuffer(other)) { + if (value.byteLength !== other.byteLength) { + return false + } + + const valueTypedArray = new Uint8Array(value) + const otherTypedArray = new Uint8Array(other) + + return valueTypedArray.every((v, i) => v === otherTypedArray[i]) + } + + if (cache.get(value) === other && cache.get(other) === value) { + return true + } + + cache.set(value, other) + cache.set(other, value) + + if ((isMap(value) && isMap(other)) || (isSet(value) && isSet(other))) { + if (value.size !== other.size) { + return false + } + + const valueArray = [...value] + const otherArray = [...other] + + return valueArray.every((v, i) => baseIsEqual(v, otherArray[i], cache)) + } + + if (isArray(value) && isArray(other)) { + if (value.length !== other.length) { + return false + } + + return value.every((v, i) => baseIsEqual(v, other[i], cache)) + } + + if (isPlainObject(value) && isPlainObject(other)) { + const valueOwnKeys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)] + const otherOwnKeys = [...Object.keys(other), ...Object.getOwnPropertySymbols(other)] + + if (valueOwnKeys.length !== otherOwnKeys.length) { + return false + } + + return valueOwnKeys.every((k) => + baseIsEqual(value[k as keyof typeof value], other[k as keyof typeof other], cache), + ) + } + + return false + } + + return baseIsEqual(value, other, cache) +} + +export function isEqual(value: any, other: any): boolean { + return isEqualWith(value, other, () => undefined) +} diff --git a/src/math.ts b/src/math.ts index 8f315dc..200e629 100644 --- a/src/math.ts +++ b/src/math.ts @@ -77,11 +77,7 @@ export function sumHash(value: any): string { .reduce((hash, key) => baseSumHash(hash, value[key], key, seen), hash) if (isFunction(value.valueOf)) { - try { - return sum(hash, String(value.valueOf())) - } catch (err) { - return sum(hash, `[valueOf exception]${(err as Error).message}`) - } + return sum(hash, String(value.valueOf())) } return hash diff --git a/tests/collection.spec.ts b/tests/collection.spec.ts index b95a59b..0f5c15e 100644 --- a/tests/collection.spec.ts +++ b/tests/collection.spec.ts @@ -298,6 +298,20 @@ describe('cloneDeep', () => { expect(value).not.toBe(result) }) + it('Error', () => { + const value = new Error() + const result = cloneDeep(value) + expect(result).toEqual({}) + expect(value).not.toBe(result) + }) + + it('DOMException', () => { + const value = new DOMException() + const result = cloneDeep(value) + expect(result).toEqual({}) + expect(value).not.toBe(result) + }) + it('class instance (prototype)', () => { class Person { name = 1 diff --git a/tests/general.spec.ts b/tests/general.spec.ts index 2dde69d..e949bcd 100644 --- a/tests/general.spec.ts +++ b/tests/general.spec.ts @@ -30,6 +30,8 @@ import { isWeakSet, isTypedArray, isDataView, + isEqual, + isEqualWith, getGlobalThis, } from '../src' @@ -268,3 +270,85 @@ it('getGlobalThis', () => { expect(getGlobalThis()).toBe(window) expect(getGlobalThis()).toBe(self) }) + +it('isEqual', () => { + expect(isEqual('123', '123')).toBe(true) + expect(isEqual('123', '1234')).toBe(false) + expect(isEqual(1, 1)).toBe(true) + expect(isEqual(1, 2)).toBe(false) + expect(isEqual(true, true)).toBe(true) + expect(isEqual(true, false)).toBe(false) + expect(isEqual(NaN, NaN)).toBe(true) + expect(isEqual(/abc/, /abc/)).toBe(true) + expect(isEqual(/abc/g, /abc/)).toBe(false) + expect(isEqual(/abc/, /abcd/)).toBe(false) + expect(isEqual(Symbol('test'), Symbol('test'))).toBe(false) + expect(isEqual(new WeakMap(), new WeakMap())).toBe(false) + expect(isEqual(new WeakSet(), new WeakSet())).toBe(false) + expect(isEqual(new Date('2024/11/03'), new Date('2024/11/03'))).toBe(true) + expect(isEqual(new Date('2024/11/03'), new Date('2024/11/04'))).toBe(false) + expect(isEqual(new Error('message'), new Error('message'))).toBe(true) + expect(isEqual(new Error('message'), new Error('mess'))).toBe(false) + expect(isEqual(new DOMException('message'), new DOMException('message'))).toBe(true) + expect(isEqual(new DOMException('message'), new DOMException('mess'))).toBe(false) + expect( + isEqual( + () => {}, + () => {}, + ), + ).toBe(false) + + class A {} + class B {} + + expect(isEqual(new A(), new A())).toBe(true) + expect(isEqual(new A(), new B())).toBe(false) + + expect(isEqual(new Set([1]), new Set([1]))).toBe(true) + expect(isEqual(new Set([1]), new Set([]))).toBe(false) + expect(isEqual(new Set([{ n: 1 }]), new Set([{ n: 1 }]))).toBe(true) + expect(isEqual(new Set([{ n: 1 }]), new Set([{ n: 2 }]))).toBe(false) + + expect(isEqual(new Map([['a', 1]]), new Map([['a', 1]]))).toBe(true) + expect(isEqual(new Map([['a', 1]]), new Map())).toBe(false) + expect(isEqual(new Map([['a', 1]]), new Map([['a', 2]]))).toBe(false) + expect(isEqual(new Map([['a', { n: 1 }]]), new Map([['a', { n: 1 }]]))).toBe(true) + expect(isEqual(new Map([[{ n: 1 }, { n: 1 }]]), new Map([[{ n: 1 }, { n: 1 }]]))).toBe(true) + expect(isEqual(new Map([[{ n: 1 }, { n: 1 }]]), new Map([[{ n: 2 }, { n: 1 }]]))).toBe(false) + + expect(isEqual(new Int8Array(8), new Int8Array(8))).toBe(true) + expect(isEqual(new Int8Array(8), new Int8Array(10))).toBe(false) + expect(isEqual(new ArrayBuffer(8), new ArrayBuffer(8))).toBe(true) + expect(isEqual(new ArrayBuffer(8), new ArrayBuffer(10))).toBe(false) + expect(isEqual(new TextEncoder().encode('123').buffer, new TextEncoder().encode('123').buffer)).toBe(true) + expect(isEqual(new TextEncoder().encode('123').buffer, new TextEncoder().encode('1234').buffer)).toBe(false) + + expect(isEqual({ n: 1 }, { n: 1 })).toBe(true) + expect(isEqual({ n: 1 }, { n: 2 })).toBe(false) + expect(isEqual({ n: 1, x: [1] }, { n: 1, x: [1] })).toBe(true) + expect(isEqual({ n: 1, x: [1] }, { n: 1, x: [1, 2] })).toBe(false) + expect(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } })).toBe(true) + expect(isEqual({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } }, a1: {} })).toBe(false) + expect(isEqual([{ a: { b: { c: 1 } } }], [{ a: { b: { c: 1 } } }])).toBe(true) + + const a: Record = { n: 1 } + a.self = a + const b: Record = { n: 1 } + b.self = b + a.x = b + b.x = a + + expect(isEqual(a, b)).toBe(true) +}) + +it('isEqualWith', () => { + expect(isEqualWith({}, [], (a, b) => typeof a === typeof b)).toBe(true) + expect(isEqualWith([1, 2, 3], [1, 2, 4], (a, b) => a.length === b.length)).toBe(true) + expect( + isEqualWith( + () => {}, + () => {}, + (a, b) => isFunction(a) === isFunction(b), + ), + ).toBe(true) +}) diff --git a/tests/math.spec.ts b/tests/math.spec.ts index 2e51c08..cb1d535 100644 --- a/tests/math.spec.ts +++ b/tests/math.spec.ts @@ -38,6 +38,8 @@ it('sumHash', () => { expect(sumHash(undefined)).toBe('29172c1a') expect(sumHash('123')).toBe('1a3a267c') expect(sumHash(123)).toBe('64a57068') + expect(sumHash({ n: 1 })).toBe('66b13e4a') + expect(sumHash(Object.create({ n: 1 }))).toBe('59322f29') expect(sumHash([1, 2, 3])).toBe('352dd8ea') expect(sumHash({ a: '123' })).toBe('b1c920ac') expect(