diff --git a/.dictionary.txt b/.dictionary.txt index 6c81c5c1..aa300ff7 100644 --- a/.dictionary.txt +++ b/.dictionary.txt @@ -21,6 +21,7 @@ gpgsign hmarr iife infile +isequal jsonifiable keyid ksort diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b542e35e..3d7368bc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -43,6 +43,10 @@ updates: - conventional-changelog-core - conventional-changelog-writer - conventional-recommended-bump + lodash: + patterns: + - '@types/lodash.*' + - lodash.* typescript-eslint: patterns: - '@typescript-eslint/*' diff --git a/__fixtures__/vehicle.ts b/__fixtures__/vehicle.ts index 05e3ae7c..d7040d6a 100644 --- a/__fixtures__/vehicle.ts +++ b/__fixtures__/vehicle.ts @@ -13,10 +13,13 @@ import type Vehicle from './types/vehicle' */ export const VEHICLE_TAG: symbol = Symbol('vehicle') -export default { - [VEHICLE_TAG]: 'vehicle', - make: faker.vehicle.manufacturer(), - model: faker.vehicle.model(), - vin: '0WBW1G4D29TC62167', - year: faker.date.past({ years: 3 }).getFullYear() -} as Opaque +export default Object.defineProperty( + { + make: faker.vehicle.manufacturer(), + model: faker.vehicle.model(), + vin: '0WBW1G4D29TC62167', + year: faker.date.past({ years: 3 }).getFullYear() + }, + VEHICLE_TAG, + { configurable: true, value: 'vehicle' } +) as Opaque diff --git a/__tests__/setup/matchers/descriptor.ts b/__tests__/setup/matchers/descriptor.ts index 5e47491b..fbdc176a 100644 --- a/__tests__/setup/matchers/descriptor.ts +++ b/__tests__/setup/matchers/descriptor.ts @@ -6,7 +6,7 @@ import type { PropertyDescriptor } from '#src/interfaces' import type { Optional } from '#src/types' import type { MatcherState, SyncExpectationResult } from '@vitest/expect' -import { dequal } from 'dequal' +import isEqual from 'lodash.isequal' /** * Asserts `target` has its own property descriptor for the given `key`. @@ -62,7 +62,7 @@ function descriptor( actual ? `${this.isNot ? 'not ' : ''}deeply equal ${expected}` : '' ].join(' ') }, - pass: dequal(actual, descriptor) + pass: isEqual(actual, descriptor) } } diff --git a/package.json b/package.json index 531941d5..5841af16 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,13 @@ "description": "TypeScript-friendly utilities", "version": "6.0.0-alpha.18", "keywords": [ + "clone", "deep", + "deep-clone", + "deep-equal", "definitions", "enums", + "equal", "get", "interfaces", "partial", @@ -81,9 +85,6 @@ "typecheck": "vitest typecheck --run", "typecheck:watch": "vitest typecheck" }, - "dependencies": { - "dequal": "2.0.3" - }, "devDependencies": { "@arethetypeswrong/cli": "0.7.1", "@commitlint/cli": "17.7.0", @@ -109,6 +110,7 @@ "@types/fs-extra": "11.0.1", "@types/git-raw-commits": "2.0.1", "@types/is-ci": "3.0.0", + "@types/lodash.isequal": "4.5.6", "@types/node-notifier": "8.0.2", "@types/prettier": "2.7.3", "@types/semver": "7.5.0", @@ -152,6 +154,7 @@ "is-ci": "3.0.1", "jsonc-eslint-parser": "2.3.0", "lint-staged": "13.2.3", + "lodash.isequal": "4.5.0", "mri": "1.2.0", "node-notifier": "10.0.1", "pkg-size": "2.4.0", diff --git a/src/utils/__tests__/equal.spec.ts b/src/utils/__tests__/equal.spec.ts index 94d97644..bd7c209e 100644 --- a/src/utils/__tests__/equal.spec.ts +++ b/src/utils/__tests__/equal.spec.ts @@ -3,34 +3,646 @@ * @module tutils/utils/tests/unit/equal */ +import FLOAT from '#fixtures/float' import INTEGER from '#fixtures/integer' +import TODAY from '#fixtures/today' +import type Vehicle from '#fixtures/types/vehicle' +import VEHICLE, { VEHICLE_TAG } from '#fixtures/vehicle' +import type { Mock } from '#tests/interfaces' +import clone from '../clone' import testSubject from '../equal' +import omit from '../omit' +import pick from '../pick' describe('unit:utils/equal', () => { - it('should return false if a and b are not deeply equal', () => { - // Arrange - const cases: Parameters[] = [ - [{}, { value: faker.number.int() }], - [[], [faker.number.int(), faker.number.int(), faker.number.int()]], - [INTEGER, INTEGER * -1], - [faker.date.future(), faker.date.past()] - ] - - // Act + Expect - cases.forEach(([a, b]) => expect(testSubject(a, b)).to.be.false) - }) - - it('should return true if a and b are deeply equal', () => { - // Arrange - const cases: Parameters[] = [ - [{ value: INTEGER }, { value: INTEGER }], - [[INTEGER], [INTEGER]], - [INTEGER, INTEGER] - ] - - // Act + Expect - cases.forEach(([a, b, identity]) => { - expect(testSubject(a, b, identity)).to.be.true + it('should return false if a and b are different types', () => { + expect(testSubject(null, VEHICLE)).to.be.false + }) + + describe('ArrayBuffer', () => { + let buffer: ArrayBuffer + + beforeAll(() => { + buffer = new ArrayBuffer(8) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(buffer, new ArrayBuffer(2))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [buffer, buffer], + [buffer, clone(buffer)], + [new ArrayBuffer(0), new ArrayBuffer(0)] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('DataView', () => { + let view: DataView + + beforeAll(() => { + view = new DataView(new ArrayBuffer(2)) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(view, new DataView(new ArrayBuffer(0)))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [view, view], + [view, clone(view)], + [new DataView(new ArrayBuffer(0)), new DataView(new ArrayBuffer(0))] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Date', () => { + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(TODAY, faker.date.future())).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [TODAY, TODAY], + [TODAY, clone(TODAY)] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Map', () => { + let map: Map + + beforeAll(() => { + map = new Map([[VEHICLE.vin, VEHICLE]]) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(map, new Map())).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [map, map], + [map, clone(map)], + [new Map(), new Map()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('RegExp', () => { + let regex: RegExp + + beforeAll(() => { + regex = /foo/ + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(regex, /bar/)).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [regex, regex], + [regex, clone(regex)], + [/^foo/, /^foo/] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Set', () => { + let set: Set + + beforeAll(() => { + set = new Set([VEHICLE]) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(set, new Set())).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [set, set], + [set, clone(set)], + [new Set(), new Set()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('TypedArray', () => { + describe('BigInt64Array', () => { + let bigint64: BigInt64Array + + beforeAll(() => { + bigint64 = new BigInt64Array(new ArrayBuffer(64), 8, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(bigint64, new BigInt64Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [bigint64, bigint64], + [bigint64, clone(bigint64)], + [new BigInt64Array(), new BigInt64Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('BigUint64Array', () => { + let biguint64: BigUint64Array + + beforeAll(() => { + biguint64 = new BigUint64Array(new ArrayBuffer(64), 8, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(biguint64, new BigUint64Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [biguint64, biguint64], + [biguint64, clone(biguint64)], + [new BigUint64Array(), new BigUint64Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Float32Array', () => { + let float32: Float32Array + + beforeAll(() => { + float32 = new Float32Array(new ArrayBuffer(32), 4, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(float32, new Float32Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [float32, float32], + [float32, clone(float32)], + [new Float32Array(), new Float32Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Float64Array', () => { + let float64: Float64Array + + beforeAll(() => { + float64 = new Float64Array(new ArrayBuffer(64), 8, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(float64, new Float64Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [float64, float64], + [float64, clone(float64)], + [new Float64Array(), new Float64Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Int8Array', () => { + let int8: Int8Array + + beforeAll(() => { + int8 = new Int8Array(new ArrayBuffer(8), 1, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(int8, new Int8Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [int8, int8], + [int8, clone(int8)], + [new Int8Array(), new Int8Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Int16Array', () => { + let int16: Int16Array + + beforeAll(() => { + int16 = new Int16Array(new ArrayBuffer(16), 2, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(int16, new Int16Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [int16, int16], + [int16, clone(int16)], + [new Int16Array(), new Int16Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Int32Array', () => { + let int32: Int32Array + + beforeAll(() => { + int32 = new Int32Array(new ArrayBuffer(32), 4, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(int32, new Int32Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [int32, int32], + [int32, clone(int32)], + [new Int32Array(), new Int32Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Uint8Array', () => { + let uint8: Uint8Array + + beforeAll(() => { + uint8 = new Uint8Array(new ArrayBuffer(8), 1, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(uint8, new Uint8Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [uint8, uint8], + [uint8, clone(uint8)], + [new Uint8Array(), new Uint8Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Uint8ClampedArray', () => { + let uint8: Uint8ClampedArray + + beforeAll(() => { + uint8 = new Uint8ClampedArray(new ArrayBuffer(8), 1, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(uint8, new Uint8ClampedArray([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [uint8, uint8], + [uint8, clone(uint8)], + [new Uint8ClampedArray(), new Uint8ClampedArray()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Uint16Array', () => { + let uint16: Uint16Array + + beforeAll(() => { + uint16 = new Uint16Array(new ArrayBuffer(16), 2, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(uint16, new Uint16Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [uint16, uint16], + [uint16, clone(uint16)], + [new Uint16Array(), new Uint16Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('Uint32Array', () => { + let uint32: Uint32Array + + beforeAll(() => { + uint32 = new Uint32Array(new ArrayBuffer(32), 4, 4) + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(uint32, new Uint32Array([]))).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [uint32, uint32], + [uint32, clone(uint32)], + [new Uint32Array(), new Uint32Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + }) + + describe('arrays', () => { + let arr: Vehicle[] + + beforeAll(() => { + arr = [VEHICLE] + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(arr, [])).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [[], []], + [arr, arr], + [arr, clone(arr)] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('error objects', () => { + let err: Error + + beforeAll(() => { + err = new Error('unknown') + }) + + it('should return false if a and b are not deeply equal', () => { + expect(testSubject(err, new Error())).to.be.false + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [err, err], + [err, clone(err)] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('functions', () => { + it('should return false if a and b are not strictly equal', () => { + expect(testSubject(vi.fn(), vi.fn())).to.be.false + }) + + it('should return true if a and b are strictly equal', () => { + // Arrange + const fn: Mock = vi.fn() + + // Act + Expect + expect(testSubject(fn, fn)).to.be.true + }) + }) + + describe('instance objects', () => { + it('should return false if a and b are not deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [Buffer.from([0x62]), Buffer.from([0x75])], + [new Int8Array(), new Uint8Array()] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.false + }) + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [Buffer.from([0x65, 0x72]), Buffer.from([0x65, 0x72])] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('pojos', () => { + let a: { next?: object } + let b: { previous?: object } + let vehicle: Vehicle + let x: { next?: object } + let y: { previous?: object } + + beforeAll(() => { + a = {} + b = {} + x = {} + y = {} + + a.next = b + b.previous = a + x.next = y + y.previous = x + + vehicle = clone(VEHICLE) + vehicle = Object.defineProperty(vehicle, '__vehicle', { value: vehicle }) + }) + + it('should return false if a and b are not deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [Object.create(null), Object.create({})], + [pick(vehicle, ['vin']), { vrm: faker.vehicle.vrm() }], + [vehicle, omit(vehicle, [VEHICLE_TAG])], + [{}, omit(vehicle, [VEHICLE_TAG])], + [{}, vehicle] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.false + }) + }) + + it('should return true if a and b are deeply equal', () => { + // Arrange + const cases: Parameters[] = [ + [Object.create(null), Object.create(null)], + [x, x], + [x, a], + [x, y.previous], + [{ data: { vehicle } }, { data: { vehicle } }] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) + }) + }) + + describe('primitives', () => { + it('should return false if a and b are not strictly equal', () => { + // Arrange + const cases: Parameters[] = [ + [0n, 1n], + [FLOAT.toString(), INTEGER.toString()], + [INTEGER, INTEGER * -1], + [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY], + [false, true], + [null, void 0] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.false + }) + }) + + it('should return true if a and b are strictly equal', () => { + // Arrange + const cases: Parameters[] = [ + [0n, 0n], + [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], + [Number.MAX_VALUE, Number.MAX_VALUE], + [Number.MIN_SAFE_INTEGER, Number.MIN_SAFE_INTEGER], + [Number.MIN_VALUE, Number.MIN_VALUE], + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.NaN, Number.NaN], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + [VEHICLE_TAG, VEHICLE_TAG], + [false, false], + [null, null], + [true, true], + [void 0, undefined] + ] + + // Act + Expect + cases.forEach(([a, b, mapper]) => { + expect(testSubject(a, b, mapper)).to.be.true + }) }) }) }) diff --git a/src/utils/equal.ts b/src/utils/equal.ts index 8db0966a..20f63ba2 100644 --- a/src/utils/equal.ts +++ b/src/utils/equal.ts @@ -3,35 +3,178 @@ * @module tutils/utils/equal */ -import type { Fn, Nilable, PropertyKey } from '#src/types' -import { dequal } from 'dequal' -import isFunction from './is-function' +import type { + Fn, + Nilable, + Objectify, + PropertyKey, + TypedArray +} from '#src/types' +import cast from './cast' +import hasOwn from './has-own' +import identity from './identity' +import isArray from './is-array' +import isArrayBuffer from './is-array-buffer' +import isDataView from './is-data-view' +import isDate from './is-date' +import isMap from './is-map' +import isObjectLike from './is-object-like' +import isRegExp from './is-reg-exp' +import isSet from './is-set' +import isTypedArray from './is-typed-array' +import iterate from './iterate' /** - * Returns a boolean indicating if values `a` and `b` are deeply equal. + * Checks if two values are deeply equal. * - * An `identity` function can be used to convert either value to a unique key. - * If provided, items with the same identity key will be considered equal. - * - * @see https://github.com/lukeed/dequal + * A `customizer` can be used to map values before comparison. It **will not** + * be called with nested property values. * * @todo examples * - * @template A - First comparison value type - * @template B - Second comparison value type - * @template K - Identity key type + * @template T - Target value type + * @template U - Comparison value type + * @template H - Customized value type * - * @param {A} a - First comparison value - * @param {B} b - Second comparison value - * @param {Nilable>} [identity] - Identity key function + * @param {T} a - Target value + * @param {U} b - Comparison value + * @param {Nilable>} [customizer=identity] - Value customizer * @return {boolean} `true` if `a` and `b` are deeply equal */ -const equal = ( - a: A, - b: B, - identity?: Nilable> +const equal = ( + a: T, + b: U, + customizer?: Nilable> ): boolean => { - return isFunction(identity) ? dequal(identity(a), identity(b)) : dequal(a, b) + /** + * Compared values cache. + * + * @const {WeakMap, Objectify>} cache + */ + const cache: WeakMap, Objectify> = new WeakMap() + + /** + * Returns an array containing own properties. + * + * @param {Objectify} obj - Target object + * @return {(string | symbol)[]} Own properties array + */ + const owned = (obj: Objectify): (string | symbol)[] => { + return [ + ...Object.getOwnPropertySymbols(obj), + ...Object.getOwnPropertyNames(obj) + ] + } + + /** + * Returns a boolean indicating if `a` and `b` are deeply equal. + * + * @param {unknown} a - Target value + * @param {unknown} b - Comparison value + * @return {boolean} `true` if `a` and `b` are deeply equal + */ + const dequal = (a: unknown, b: unknown): boolean => { + // exit early if a and b are strictly equal + if (a === b) return true + + // exit early if a or b is not object like + if (!isObjectLike(a) || !isObjectLike(b)) return a !== a && b !== b + + // exit early on constructor mismatch + if (a.constructor !== b.constructor) return false + + // compare cyclic values + if (cache.get(a) && cache.get(b)) { + return cache.get(a) === b && cache.get(b) === a + } + + // cache objects + cache.set(a, b) + cache.set(b, a) + + // compare arrays + if (isArray(a) && isArray(b)) { + return a.length === b.length + ? iterate(a.length, true, (acc: boolean, i: number) => { + return acc && dequal(cast(a)[i], cast(b)[i]) + }) + : false + } + + // compare array buffers + if (isArrayBuffer(a) && isArrayBuffer(b)) { + return dequal(new Uint8Array(a), new Uint8Array(b)) + } + + // compare data views + if (isDataView(a) && isDataView(b)) { + return a.byteLength === b.byteLength + ? iterate(a.byteLength, true, (acc: boolean, i: number) => { + return ( + acc && + cast(a).getInt8(i) === cast(b).getInt8(i) + ) + }) + : false + } + + // compare dates + if (isDate(a) && isDate(b)) return a.getTime() === b.getTime() + + // compare maps + if (isMap(a) && isMap(b)) { + return a.size === b.size + ? [...a.entries()].reduce((acc, [key, value]) => { + return acc && isMap(b) && b.has(key) && dequal(value, b.get(key)) + }, true) + : false + } + + // compare regular expressions + if (isRegExp(a) && isRegExp(b)) return a.toString() === b.toString() + + // compare sets + if (isSet(a) && isSet(b)) { + return a.size === b.size + ? [...a].reduce((acc, value) => { + return acc && [...cast>(b)].some(v => dequal(value, v)) + }, true) + : false + } + + // compare typed arrays + if (isTypedArray(a) && isTypedArray(b)) { + return a.length === b.length + ? iterate(a.length, true, (acc: boolean, i: number) => { + return acc && cast(a)[i] === cast(b)[i] + }) + : false + } + + /** + * Own properties of {@linkcode a}. + * + * @const {(string | symbol)[]} properties + */ + const properties: (string | symbol)[] = owned(a) + + // compare property values + switch (true) { + case properties.length !== owned(b).length: + return false + default: + for (const p of properties) { + if (p === 'stack' && a instanceof Error) continue + if (!hasOwn(b, p)) return false + if (!dequal(Reflect.get(a, p), Reflect.get(b, p))) return false + } + } + + return true + } + + customizer ??= cast>(identity) + return dequal(customizer(a), customizer(b)) } export default equal diff --git a/yarn.lock b/yarn.lock index 47d1f2bc..b3d3895e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,6 +1594,7 @@ __metadata: "@types/fs-extra": "npm:11.0.1" "@types/git-raw-commits": "npm:2.0.1" "@types/is-ci": "npm:3.0.0" + "@types/lodash.isequal": "npm:4.5.6" "@types/node-notifier": "npm:8.0.2" "@types/prettier": "npm:2.7.3" "@types/semver": "npm:7.5.0" @@ -1614,7 +1615,6 @@ __metadata: conventional-recommended-bump: "npm:7.0.1" cross-env: "npm:7.0.3" cspell: "npm:7.0.0-alpha.2" - dequal: "npm:2.0.3" esbuild: "npm:0.19.0" eslint: "npm:8.46.0" eslint-config-prettier: "npm:9.0.0" @@ -1638,6 +1638,7 @@ __metadata: is-ci: "npm:3.0.1" jsonc-eslint-parser: "npm:2.3.0" lint-staged: "npm:13.2.3" + lodash.isequal: "npm:4.5.0" mri: "npm:1.2.0" node-notifier: "npm:10.0.1" pkg-size: "npm:2.4.0" @@ -2640,6 +2641,22 @@ __metadata: languageName: node linkType: hard +"@types/lodash.isequal@npm:4.5.6": + version: 4.5.6 + resolution: "@types/lodash.isequal@npm:4.5.6" + dependencies: + "@types/lodash": "npm:*" + checksum: fd828fc4362ca9ddb5502dcee45f9f21594ad8f27d7d9f8bb57ec3ecad9a00bc353c139cc9dd47c64e4b550386d83a16820c3de1e7f939216b1b1ecd4db88b97 + languageName: node + linkType: hard + +"@types/lodash@npm:*": + version: 4.14.197 + resolution: "@types/lodash@npm:4.14.197" + checksum: 3e2b11acbb2ed88123127757bba04c193682c9cb4cfa644432cb7c4eccb7799fd2d651ca542391916fe3e87c01cee5f15e4b7772a1a92d16b95cf4bd70074e6c + languageName: node + linkType: hard + "@types/mdast@npm:^3.0.0": version: 3.0.10 resolution: "@types/mdast@npm:3.0.10" @@ -7051,6 +7068,13 @@ __metadata: languageName: node linkType: hard +"lodash.isequal@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.isequal@npm:4.5.0" + checksum: d413585fd1dbcb6ebed6d9d98a7fe5e10cc6f653b6676a61a69185aa1f6ae5b4aac1367a632db0ee46197ee1ab6aa5b7428c1721f9b5459a3623e0cc930b9b77 + languageName: node + linkType: hard + "lodash.isfunction@npm:^3.0.9": version: 3.0.9 resolution: "lodash.isfunction@npm:3.0.9"