feat(isEqual): Implement isEqual

This commit is contained in:
raon0211 2024-08-07 18:50:11 +09:00
parent db79ff3355
commit ce412ff996
15 changed files with 1133 additions and 124 deletions

View File

@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest"
import { getBundleSize } from "./utils/getBundleSize"
describe('isEqual bundle size', () => {
it('lodash-es', async () => {
const bundleSize = await getBundleSize('lodash-es', 'isEqual');
expect(bundleSize).toMatchInlineSnapshot(`12872`);
});
it('es-toolkit', async () => {
const bundleSize = await getBundleSize('es-toolkit', 'isEqual');
expect(bundleSize).toMatchInlineSnapshot(`2728`);
})
});

View File

@ -9,6 +9,6 @@ describe('zipObjectDeep bundle size', () => {
it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'zipObjectDeep');
expect(bundleSize).toMatchInlineSnapshot(`938`);
expect(bundleSize).toMatchInlineSnapshot(`992`);
})
});

View File

@ -9,7 +9,7 @@
"esbuild": "0.23.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"vitest": "^2.0.2"
"vitest": "^2.0.5"
},
"devDependencies": {
"@types/lodash": "^4",

View File

@ -2,54 +2,81 @@ import { bench, describe } from 'vitest';
import { isEqual as isEqualToolkit } from 'es-toolkit';
import { isEqual as isEqualLodash } from 'lodash';
describe('isEqual', () => {
// describe('isEqual primitives', () => {
// bench('es-toolkit/isEqual', () => {
// isEqualToolkit(1, 1);
// isEqualToolkit(NaN, NaN);
// isEqualToolkit(+0, -0);
// isEqualToolkit(true, true);
// isEqualToolkit(true, false);
// isEqualToolkit('hello', 'hello');
// isEqualToolkit('hello', 'world');
// });
// bench('lodash/isEqual', () => {
// isEqualLodash(1, 1);
// isEqualLodash(NaN, NaN);
// isEqualLodash(+0, -0);
// isEqualLodash(true, true);
// isEqualLodash(true, false);
// isEqualLodash('hello', 'hello');
// isEqualLodash('hello', 'world');
// });
// });
// describe('isEqual dates', () => {
// bench('es-toolkit/isEqual', () => {
// isEqualToolkit(new Date('2020-01-01'), new Date('2020-01-01'));
// isEqualToolkit(new Date('2020-01-01'), new Date('2021-01-01'));
// });
// bench('lodash', () => {
// isEqualLodash(new Date('2020-01-01'), new Date('2020-01-01'));
// isEqualLodash(new Date('2020-01-01'), new Date('2021-01-01'));
// });
// });
// describe('isEqual RegExps', () => {
// bench('es-toolkit/isEqual', () => {
// isEqualToolkit(/hello/g, /hello/g);
// isEqualToolkit(/hello/g, /hello/i);
// });
// bench('lodash', () => {
// isEqualLodash(/hello/g, /hello/g);
// isEqualLodash(/hello/g, /hello/i);
// })
// })
describe('isEqual objects', () => {
bench('es-toolkit/isEqual', () => {
isEqualToolkit(1, 1);
isEqualToolkit('hello', 'hello');
isEqualToolkit(true, true);
isEqualToolkit('hello', 'world');
isEqualToolkit(true, false);
isEqualToolkit(NaN, NaN);
isEqualToolkit(+0, -0);
isEqualToolkit(new Date('2020-01-01'), new Date('2020-01-01'));
isEqualToolkit(new Date('2020-01-01'), new Date('2021-01-01'));
isEqualToolkit(/hello/g, /hello/g);
isEqualToolkit(/hello/g, /hello/i);
isEqualToolkit({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } });
isEqualToolkit({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } });
isEqualToolkit({ a: 1, b: 2 }, { a: 1, b: 2 });
isEqualToolkit([1, 2, 3], [1, 2, 3]);
isEqualToolkit([1, 2, 3], [1, 2, 4]);
});
bench('lodash/isEqual', () => {
isEqualLodash(1, 1);
isEqualLodash('hello', 'hello');
isEqualLodash(true, true);
isEqualLodash('hello', 'world');
isEqualLodash(true, false);
isEqualLodash(NaN, NaN);
isEqualLodash(+0, -0);
isEqualLodash(new Date('2020-01-01'), new Date('2020-01-01'));
isEqualLodash(new Date('2020-01-01'), new Date('2021-01-01'));
isEqualLodash(/hello/g, /hello/g);
isEqualLodash(/hello/g, /hello/i);
bench('lodash', () => {
isEqualLodash({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } });
isEqualLodash({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } });
isEqualLodash({ a: 1, b: 2 }, { a: 1, b: 2 });
isEqualLodash([1, 2, 3], [1, 2, 3]);
isEqualLodash([1, 2, 3], [1, 2, 4]);
});
});
})
// describe('isEqual arrays', () => {
// bench('es-toolkit/isEqual', () => {
// isEqualToolkit([1, 2, 3], [1, 2, 3]);
// isEqualToolkit([1, 2, 3], [1, 2, 4]);
// });
// bench('lodash', () => {
// isEqualLodash([1, 2, 3], [1, 2, 3]);
// isEqualLodash([1, 2, 3], [1, 2, 4]);
// });
// })

View File

@ -204,7 +204,7 @@ Even if a feature is marked "in review," it might already be under review to ens
| [isDate](https://lodash.com/docs/4.17.15#isDate) | ❌ |
| [isElement](https://lodash.com/docs/4.17.15#isElement) | ❌ |
| [isEmpty](https://lodash.com/docs/4.17.15#isEmpty) | ❌ |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqualWith](https://lodash.com/docs/4.17.15#isEqualWith) | ❌ |
| [isError](https://lodash.com/docs/4.17.15#isError) | ❌ |
| [isFinite](https://lodash.com/docs/4.17.15#isFinite) | ❌ |

View File

@ -205,7 +205,7 @@ chunk([1, 2, 3, 4], 0);
| [isDate](https://lodash.com/docs/4.17.15#isDate) | ❌ |
| [isElement](https://lodash.com/docs/4.17.15#isElement) | ❌ |
| [isEmpty](https://lodash.com/docs/4.17.15#isEmpty) | ❌ |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqualWith](https://lodash.com/docs/4.17.15#isEqualWith) | ❌ |
| [isError](https://lodash.com/docs/4.17.15#isError) | ❌ |
| [isFinite](https://lodash.com/docs/4.17.15#isFinite) | ❌ |

View File

@ -204,7 +204,7 @@ chunk([1, 2, 3, 4], 0);
| [isDate](https://lodash.com/docs/4.17.15#isDate) | ❌ |
| [isElement](https://lodash.com/docs/4.17.15#isElement) | ❌ |
| [isEmpty](https://lodash.com/docs/4.17.15#isEmpty) | ❌ |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqual](https://lodash.com/docs/4.17.15#isEqual) | |
| [isEqualWith](https://lodash.com/docs/4.17.15#isEqualWith) | ❌ |
| [isError](https://lodash.com/docs/4.17.15#isError) | ❌ |
| [isFinite](https://lodash.com/docs/4.17.15#isFinite) | ❌ |

View File

@ -0,0 +1,6 @@
import { typedArrays } from "./typedArrays";
export const arrayViews = [
...typedArrays,
'DataView'
];

View File

@ -0,0 +1,4 @@
export function getSymbols(object: {}) {
return Object.getOwnPropertySymbols(object)
.filter(symbol => object.propertyIsEnumerable(symbol));
}

View File

@ -0,0 +1,26 @@
export const regexpTag = '[object RegExp]';
export const stringTag = '[object String]';
export const numberTag = '[object Number]';
export const booleanTag = '[object Boolean]';
export const argumentsTag = '[object Arguments]';
export const symbolTag = '[object Symbol]';
export const dateTag = '[object Date]';
export const mapTag = '[object Map]';
export const setTag = '[object Set]';
export const arrayTag = '[object Array]';
export const functionTag = '[object Function]';
export const arrayBufferTag = '[object ArrayBuffer]';
export const objectTag = '[object Object]';
export const errorTag = '[object Error]';
export const dataViewTag = '[object DataView]'
export const uint8ArrayTag = '[object Uint8Array]';
export const uint8ClampedArrayTag = '[object Uint8ClampedArray]';
export const uint16ArrayTag = '[object Uint16Array]';
export const uint32ArrayTag = '[object Uint32Array]';
export const bigUint64ArrayTag = '[object BigUint64Array]';
export const int8ArrayTag = '[object Int8Array]';
export const int16ArrayTag = '[object Int16Array]';
export const int32ArrayTag = '[object Int32Array]';
export const bigInt64ArrayTag = '[object BigInt64Array]';
export const float32ArrayTag = '[object Float32Array]';
export const float64ArrayTag = '[object Float64Array]';

View File

@ -1,4 +1,5 @@
import { cloneDeep as cloneDeepToolkit, copyProperties } from '../../object/cloneDeep.ts';
import { argumentsTag, booleanTag, numberTag, stringTag } from '../_internal/tags.ts';
/**
* Creates a deep clone of the given object.
@ -52,9 +53,9 @@ export function cloneDeep<T>(obj: T): T {
}
switch (Object.prototype.toString.call(obj)) {
case '[object Number]':
case '[object String]':
case '[object Boolean]': {
case numberTag:
case stringTag:
case booleanTag: {
// eslint-disable-next-line
// @ts-ignore
const result = new obj.constructor(obj?.valueOf()) as T;
@ -62,7 +63,7 @@ export function cloneDeep<T>(obj: T): T {
return result;
}
case '[object Arguments]': {
case argumentsTag: {
const result = {} as any;
copyProperties(result, obj);

View File

@ -0,0 +1,736 @@
import { describe, expect, it } from "vitest";
import { noop } from "../../function/noop";
import { stubFalse } from "../_internal/stubFalse";
import { isEqual } from "es-toolkit/compat";
import { args } from "../_internal/args";
import { arrayViews } from "../_internal/arrayViews";
describe('isEqual', () => {
const symbol1 = Symbol ? Symbol('a') : true;
const symbol2 = Symbol ? Symbol('b') : false;
it('should compare primitives', () => {
const pairs = [
[1, 1, true],
[1, Object(1), true],
[1, '1', false],
[1, 2, false],
[-0, -0, true],
[0, 0, true],
[0, Object(0), true],
[Object(0), Object(0), true],
[-0, 0, true],
[0, '0', false],
[0, null, false],
[NaN, NaN, true],
[NaN, Object(NaN), true],
[Object(NaN), Object(NaN), true],
[NaN, 'a', false],
[NaN, Infinity, false],
['a', 'a', true],
['a', Object('a'), true],
[Object('a'), Object('a'), true],
['a', 'b', false],
['a', ['a'], false],
[true, true, true],
[true, Object(true), true],
[Object(true), Object(true), true],
[true, 1, false],
[true, 'a', false],
[false, false, true],
[false, Object(false), true],
[Object(false), Object(false), true],
[false, 0, false],
[false, '', false],
[symbol1, symbol1, true],
[symbol1, Object(symbol1), true],
[Object(symbol1), Object(symbol1), true],
[symbol1, symbol2, false],
[null, null, true],
[null, undefined, false],
[null, {}, false],
[null, '', false],
[undefined, undefined, true],
[undefined, null, false],
[undefined, '', false],
];
const expected = pairs.map((pair) => pair[2]);
const actual = pairs.map((pair) => isEqual(pair[0], pair[1]));
expect(actual).toEqual(expected);
});
it('should compare arrays', () => {
let array1: unknown[] = [true, null, 1, 'a', undefined];
let array2: unknown[] = [true, null, 1, 'a', undefined];
expect(isEqual(array1, array2)).toBe(true);
array1 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }];
array2 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }];
expect(isEqual(array1, array2)).toBe(true);
array1 = [1];
array1[2] = 3;
array2 = [1];
array2[1] = undefined;
array2[2] = 3;
expect(isEqual(array1, array2)).toBe(true);
array1 = [
Object(1),
false,
Object('a'),
/x/,
new Date(2012, 4, 23),
['a', 'b', [Object('c')]],
{ a: 1 },
];
array2 = [
1,
Object(false),
'a',
/x/,
new Date(2012, 4, 23),
['a', Object('b'), ['c']],
{ a: 1 },
];
expect(isEqual(array1, array2)).toBe(true);
array1 = [1, 2, 3];
array2 = [3, 2, 1];
expect(isEqual(array1, array2)).toBe(false);
array1 = [1, 2];
array2 = [1, 2, 3];
expect(isEqual(array1, array2)).toBe(false);
});
it('should treat arrays with identical values but different non-index properties as equal', () => {
let array1: any = [1, 2, 3];
let array2: any = [1, 2, 3];
array1.every =
array1.filter =
array1.forEach =
array1.indexOf =
array1.lastIndexOf =
array1.map =
array1.some =
array1.reduce =
array1.reduceRight =
null;
array2.concat =
array2.join =
array2.pop =
array2.reverse =
array2.shift =
array2.slice =
array2.sort =
array2.splice =
array2.unshift =
null;
expect(isEqual(array1, array2)).toBe(true);
array1 = [1, 2, 3];
array1.a = 1;
array2 = [1, 2, 3];
array2.b = 1;
expect(isEqual(array1, array2)).toBe(true);
array1 = /c/.exec('abcde');
array2 = ['c'];
expect(isEqual(array1, array2)).toBe(true);
});
it('should compare sparse arrays', () => {
const array = Array(1);
expect(isEqual(array, Array(1))).toBe(true);
expect(isEqual(array, [undefined])).toBe(true);
expect(isEqual(array, Array(2))).toBe(false);
});
it('should compare plain objects', () => {
let object1: any = { a: true, b: null, c: 1, d: 'a', e: undefined };
let object2: any = { a: true, b: null, c: 1, d: 'a', e: undefined };
expect(isEqual(object1, object2)).toBe(true);
object1 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } };
object2 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } };
expect(isEqual(object1, object2)).toBe(true);
object1 = { a: 1, b: 2, c: 3 };
object2 = { a: 3, b: 2, c: 1 };
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2, c: 3 };
object2 = { d: 1, e: 2, f: 3 };
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2 };
object2 = { a: 1, b: 2, c: 3 };
expect(isEqual(object1, object2)).toBe(false);
});
it('should compare objects regardless of key order', () => {
const object1 = { a: 1, b: 2, c: 3 };
const object2 = { c: 3, a: 1, b: 2 };
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare nested objects', () => {
const object1 = {
a: [1, 2, 3],
b: true,
c: Object(1),
d: 'a',
e: {
f: ['a', Object('b'), 'c'],
g: Object(false),
h: new Date(2012, 4, 23),
i: noop,
j: 'a',
},
};
const object2 = {
a: [1, Object(2), 3],
b: Object(true),
c: 1,
d: Object('a'),
e: {
f: ['a', 'b', 'c'],
g: false,
h: new Date(2012, 4, 23),
i: noop,
j: 'a',
},
};
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare object instances', () => {
function Foo() {
// eslint-disable-next-line
// @ts-ignore
this.a = 1;
}
Foo.prototype.a = 1;
function Bar() {
// eslint-disable-next-line
// @ts-ignore
this.a = 1;
}
Bar.prototype.a = 2;
// eslint-disable-next-line
// @ts-ignore
expect(isEqual(new Foo(), new Foo())).toBe(true);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual(new Foo(), new Bar())).toBe(false);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual({ a: 1 }, new Foo())).toBe(false);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual({ a: 2 }, new Bar())).toBe(false);
});
it('should compare objects with constructor properties', () => {
expect(isEqual({ constructor: 1 }, { constructor: 1 })).toBe(true);
expect(isEqual({ constructor: 1 }, { constructor: '1' })).toBe(false);
expect(isEqual({ constructor: [1] }, { constructor: [1] })).toBe(true);
expect(isEqual({ constructor: [1] }, { constructor: ['1'] })).toBe(false);
expect(isEqual({ constructor: Object }, {})).toBe(false);
});
it('should compare arrays with circular references', () => {
let array1: any[] = [];
let array2: any[] = [];
array1.push(array1);
array2.push(array2);
expect(isEqual(array1, array2)).toBe(true);
array1.push('b');
array2.push('b');
expect(isEqual(array1, array2)).toBe(true);
array1.push('c');
array2.push('d');
expect(isEqual(array1, array2)).toBe(false);
array1 = ['a', 'b', 'c'];
array1[1] = array1;
array2 = ['a', ['a', 'b', 'c'], 'c'];
expect(isEqual(array1, array2)).toBe(false);
});
it('should have transitive equivalence for circular references of arrays', () => {
const array1: any[] = [];
const array2: any[] = [array1];
const array3: any[] = [array2];
array1[0] = array1;
expect(isEqual(array1, array2)).toBe(true);
expect(isEqual(array2, array3)).toBe(true);
expect(isEqual(array1, array3)).toBe(true);
});
it('should compare objects with circular references', () => {
let object1: any = {};
let object2: any = {};
object1.a = object1;
object2.a = object2;
expect(isEqual(object1, object2)).toBe(true);
object1.b = 0;
object2.b = Object(0);
expect(isEqual(object1, object2)).toBe(true);
object1.c = Object(1);
object2.c = Object(2);
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2, c: 3 };
object1.b = object1;
object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 };
expect(isEqual(object1, object2)).toBe(false);
});
it('should have transitive equivalence for circular references of objects', () => {
const object1: any = {};
const object2: any = { a: object1 };
const object3: any = { a: object2 };
object1.a = object1;
expect(isEqual(object1, object2)).toBe(true);
expect(isEqual(object2, object3)).toBe(true);
expect(isEqual(object1, object3)).toBe(true);
});
it('should compare objects with multiple circular references', () => {
const array1: any = [{}];
const array2: any = [{}];
(array1[0].a = array1).push(array1);
(array2[0].a = array2).push(array2);
expect(isEqual(array1, array2)).toBe(true);
array1[0].b = 0;
array2[0].b = Object(0);
expect(isEqual(array1, array2)).toBe(true);
array1[0].c = Object(1);
array2[0].c = Object(2);
expect(isEqual(array1, array2)).toBe(false);
});
it('should compare objects with complex circular references', () => {
const object1: any = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
const object2: any = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
object1.foo.b.c.d = object1;
object1.bar.b = object1.foo.b;
object2.foo.b.c.d = object2;
object2.bar.b = object2.foo.b;
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare objects with shared property values', () => {
const object1: any = {
a: [1, 2],
};
const object2: any = {
a: [1, 2],
b: [1, 2],
};
object1.b = object1.a;
expect(isEqual(object1, object2)).toBe(true);
});
it('should treat objects created by `Object.create(null)` like plain objects', () => {
function Foo() {
// eslint-disable-next-line
// @ts-ignore
this.a = 1;
}
Foo.prototype.constructor = null;
const object1 = Object.create(null);
object1.a = 1;
const object2 = { a: 1 };
expect(isEqual(object1, object2)).toBe(true);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual(new Foo(), object2)).toBe(false);
});
it('should avoid common type coercions', () => {
expect(isEqual(true, Object(false))).toBe(false);
expect(isEqual(Object(false), Object(0))).toBe(false);
expect(isEqual(false, Object(''))).toBe(false);
expect(isEqual(Object(36), Object('36'))).toBe(false);
expect(isEqual(0, '')).toBe(false);
expect(isEqual(1, true)).toBe(false);
expect(isEqual(1337756400000, new Date(2012, 4, 23))).toBe(false);
expect(isEqual('36', 36)).toBe(false);
expect(isEqual(36, '36')).toBe(false);
});
it('should compare `arguments` objects', () => {
const args1 = (function () {
return arguments;
})();
const args2 = (function () {
return arguments;
})();
const args3 = (function (..._: any[]) {
return arguments;
})(1, 2);
expect(isEqual(args1, args2)).toBe(true);
expect(isEqual(args1, args3)).toBe(false);
});
it('should treat `arguments` objects like `Object` objects', () => {
const object = { 0: 1, 1: 2, 2: 3 };
function Foo() { }
Foo.prototype = object;
expect(isEqual(args, object)).toBe(true);
expect(isEqual(object, args)).toBe(true);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual(args, new Foo())).toBe(false);
// eslint-disable-next-line
// @ts-ignore
expect(isEqual(new Foo(), args)).toBe(false);
});
it('should compare array buffers', () => {
const buffer = new Int8Array([-1]).buffer;
expect(isEqual(buffer, new Uint8Array([255]).buffer)).toBe(true);
expect(isEqual(buffer, new ArrayBuffer(1))).toBe(false);
});
it('should compare array views', () => {
const pairs = arrayViews.map((type, viewIndex) => {
const otherType = arrayViews[(viewIndex + 1) % arrayViews.length];
const CtorA =
// eslint-disable-next-line
// @ts-ignore
globalThis[type] ||
// eslint-disable-next-line
// @ts-ignore
function (n) {
// eslint-disable-next-line
// @ts-ignore
this.n = n;
};
const CtorB =
// eslint-disable-next-line
// @ts-ignore
globalThis[otherType] ||
// eslint-disable-next-line
// @ts-ignore
function (n) {
// eslint-disable-next-line
// @ts-ignore
this.n = n;
};
// eslint-disable-next-line
// @ts-ignore
const bufferA = globalThis[type] ? new ArrayBuffer(8) : 8;
// eslint-disable-next-line
// @ts-ignore
const bufferB = globalThis[otherType] ? new ArrayBuffer(8) : 8;
// eslint-disable-next-line
// @ts-ignore
const bufferC = globalThis[otherType] ? new ArrayBuffer(16) : 16;
return [
new CtorA(bufferA),
new CtorA(bufferA),
new CtorB(bufferB),
new CtorB(bufferC),
];
});
const expected = pairs.map(() => [true, false, false]);
const actual = pairs.map((pair) => [
isEqual(pair[0], pair[1]),
isEqual(pair[0], pair[2]),
isEqual(pair[2], pair[3]),
]);
expect(actual).toEqual(expected);
});
it('should compare buffers', () => {
const buffer = Buffer.from([1]);
expect(isEqual(buffer, Buffer.from([1]))).toBe(true);
expect(isEqual(buffer, Buffer.from([2]))).toBe(false);
expect(isEqual(buffer, new Uint8Array([1]))).toBe(false);
});
it('should compare date objects', () => {
const date = new Date(2012, 4, 23);
expect(isEqual(date, new Date(2012, 4, 23))).toBe(true);
expect(isEqual(new Date('a'), new Date('b'))).toBe(true);
expect(isEqual(date, new Date(2013, 3, 25))).toBe(false);
expect(isEqual(date, { getTime: () => +date })).toBe(false);
});
it('should compare error objects', () => {
const pairs = [
'Error',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
].map((type, index, errorTypes) => {
const otherType = errorTypes[++index % errorTypes.length];
// eslint-disable-next-line
// @ts-ignore
const CtorA = globalThis[type];
// eslint-disable-next-line
// @ts-ignore
const CtorB = globalThis[otherType];
return [new CtorA('a'), new CtorA('a'), new CtorB('a'), new CtorB('b')];
});
const expected = pairs.map(() => [true, false, false]);
const actual = pairs.map((pair) => [
isEqual(pair[0], pair[1]),
isEqual(pair[0], pair[2]),
isEqual(pair[2], pair[3]),
]);
expect(actual).toEqual(expected);
});
it('should compare functions', () => {
function a() {
return 1 + 2;
}
function b() {
return 1 + 2;
}
expect(isEqual(a, a)).toBe(true);
expect(isEqual(a, b)).toBe(false);
});
it('should compare maps', () => {
[
[new Map(), new Map()],
].forEach((maps) => {
const map1 = maps[0];
const map2 = maps[1];
map1.set('a', 1);
map2.set('b', 2);
expect(isEqual(map1, map2)).toBe(false);
map1.set('b', 2);
map2.set('a', 1);
expect(isEqual(map1, map2)).toBe(true);
map1.delete('a');
map1.set('a', 1);
expect(isEqual(map1, map2)).toBe(true);
map2.delete('a');
expect(isEqual(map1, map2)).toBe(false);
map1.clear();
map2.clear();
});
});
it('should compare maps with circular references', () => {
const map1 = new Map();
const map2 = new Map();
map1.set('a', map1);
map2.set('a', map2);
expect(isEqual(map1, map2)).toBe(true);
map1.set('b', 1);
map2.set('b', 2);
expect(isEqual(map1, map2)).toBe(false);
});
it('should compare promises by reference', () => {
[
[Promise.resolve(1), Promise.resolve(1)],
].forEach(
(promises) => {
const promise1 = promises[0];
const promise2 = promises[1];
expect(isEqual(promise1, promise2)).toBe(false);
expect(isEqual(promise1, promise1)).toBe(true);
});
});
it('should compare regexes', () => {
expect(isEqual(/x/gim, /x/gim)).toBe(true);
expect(isEqual(/x/gim, /x/gim)).toBe(true);
expect(isEqual(/x/gi, /x/g)).toBe(false);
expect(isEqual(/x/, /y/)).toBe(false);
expect(
isEqual(/x/g, { global: true, ignoreCase: false, multiline: false, source: 'x' })
).toBe(false);
});
it('should compare sets', () => {
[
[new Set(), new Set()],
].forEach((sets) => {
const set1 = sets[0];
const set2 = sets[1];
set1.add(1);
set2.add(2);
expect(isEqual(set1, set2)).toBe(false);
set1.add(2);
set2.add(1);
expect(isEqual(set1, set2)).toBe(true);
set1.delete(1);
set1.add(1);
expect(isEqual(set1, set2)).toBe(true);
set2.delete(1);
expect(isEqual(set1, set2)).toBe(false);
set1.clear();
set2.clear();
});
});
it('should compare sets with circular references', () => {
const set1 = new Set();
const set2 = new Set();
set1.add(set1);
set2.add(set2);
expect(isEqual(set1, set2)).toBe(true);
set1.add(1);
set2.add(2);
expect(isEqual(set1, set2)).toBe(false);
});
it('should compare symbol properties', () => {
const symbol1 = Symbol('a');
const symbol2 = Symbol('b');
const object1: any = { a: 1 };
const object2: any = { a: 1 };
object1[symbol1] = { a: { b: 2 } };
object2[symbol1] = { a: { b: 2 } };
Object.defineProperty(object2, symbol2, {
configurable: true,
enumerable: false,
writable: true,
value: 2,
});
expect(isEqual(object1, object2)).toBe(true);
object2[symbol1] = { a: 1 };
expect(isEqual(object1, object2)).toBe(false);
delete object2[symbol1];
object2[Symbol('a')] = { a: { b: 2 } };
expect(isEqual(object1, object2)).toBe(false);
});
it('should return `false` for objects with custom `toString` methods', () => {
let primitive: any;
const object = {
toString: function () {
return primitive;
},
};
const values = [true, null, 1, 'a', undefined];
const expected = values.map(stubFalse);
const actual = values.map((value) => {
primitive = value;
return isEqual(object, value);
});
expect(actual).toEqual(expected);
});
});

View File

@ -18,8 +18,8 @@ describe('isEqual', () => {
expect(isEqual(NaN, NaN)).toBe(true);
});
it('should return false for +0 and -0 comparisons', () => {
expect(isEqual(+0, -0)).toBe(false);
it('should return true for +0 and -0 comparisons', () => {
expect(isEqual(+0, -0)).toBe(true);
});
it('should return true for equal Date objects', () => {

View File

@ -1,4 +1,8 @@
import { union } from "../array/union.ts";
import { argumentsTag, arrayBufferTag, arrayTag, bigInt64ArrayTag, bigUint64ArrayTag, booleanTag, dataViewTag, dateTag, errorTag, float32ArrayTag, float64ArrayTag, functionTag, int16ArrayTag, int32ArrayTag, int8ArrayTag, mapTag, numberTag, objectTag, regexpTag, setTag, stringTag, symbolTag, uint16ArrayTag, uint32ArrayTag, uint8ArrayTag, uint8ClampedArrayTag } from "../compat/_internal/tags.ts";
import { getSymbols } from "../compat/_internal/getSymbols.ts";
import { getTag } from "../compat/_internal/getTag.ts";
import { isPlainObject } from "./isPlainObject.ts";
/**
* Checks if two values are equal, including support for `Date`, `RegExp`, and deep object comparison.
@ -14,44 +18,223 @@ import { union } from "../array/union.ts";
* isEqual(new Date('2020-01-01'), new Date('2020-01-01')); // true
* isEqual([1, 2, 3], [1, 2, 3]); // true
*/
export function isEqual(a: unknown, b: unknown): boolean {
export function isEqual(a: any, b: any): boolean {
if (typeof a === typeof b) {
switch (typeof a) {
case 'bigint':
case 'string':
case 'boolean':
case 'symbol':
case 'undefined': {
return a === b;
}
case 'number': {
return a === b || Object.is(a, b);
}
case 'function': {
return a === b;
}
case 'object': {
return areObjectsEqual(a, b);
}
}
}
return areObjectsEqual(a, b);
}
function areObjectsEqual(a: any, b: any, stack?: Map<any, any>) {
if (Object.is(a, b)) {
return true;
}
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
let aTag = getTag(a);
let bTag = getTag(b);
if (aTag === argumentsTag) {
aTag = objectTag;
}
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
if (bTag === argumentsTag) {
bTag = objectTag;
}
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
if (aTag !== bTag) {
return false;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
switch (aTag) {
case regexpTag:
case stringTag:
return a.toString() === b.toString();
if (aKeys.length !== bKeys.length) {
return false;
}
case numberTag: {
const x = a.valueOf();
const y = b.valueOf();
// check if all keys in both arrays match
if (union(aKeys, bKeys).length !== aKeys.length) {
return false;
}
return x === y || Number.isNaN(x) && Number.isNaN(y);
}
for (let i = 0; i < aKeys.length; i++) {
const propKey = aKeys[i];
const aProp = (a as any)[propKey];
const bProp = (b as any)[propKey];
if (!isEqual(aProp, bProp)) {
return false;
case booleanTag:
case dateTag:
case symbolTag:
return Object.is(a.valueOf(), b.valueOf());
case regexpTag: {
return a.source === b.source && a.flags === b.flags;
}
case functionTag: {
return a == b;
}
}
return true;
stack = stack ?? new Map();
const aStack = stack.get(a);
const bStack = stack.get(b);
if (aStack != null && bStack != null) {
return aStack === b;
}
stack.set(a, b);
stack.set(b, a);
try {
switch (aTag) {
case mapTag: {
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a.entries()) {
if (!b.has(key) || !areObjectsEqual(value, b.get(key), stack)) {
return false;
}
}
return true;
}
case setTag: {
if (a.size !== b.size) {
return false;
}
const aValues = Array.from(a.values());
const bValues = Array.from(b.values());
for (let i = 0; i < aValues.length; i++) {
const aValue = aValues[i];
const index = bValues.findIndex(bValue => {
return areObjectsEqual(aValue, bValue, stack);
})
if (index === -1) {
return false;
}
bValues.splice(index, 1);
}
return true;
}
case arrayTag:
case uint8ArrayTag:
case uint8ClampedArrayTag:
case uint16ArrayTag:
case uint32ArrayTag:
case bigUint64ArrayTag:
case int8ArrayTag:
case int16ArrayTag:
case int32ArrayTag:
case bigInt64ArrayTag:
case float32ArrayTag:
case float64ArrayTag: {
// Buffers are also treated as [object Uint8Array]s.
if (Buffer.isBuffer(a) !== Buffer.isBuffer(b)) {
return false;
}
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!areObjectsEqual(a[i], b[i], stack)) {
return false;
}
}
return true;
}
case arrayBufferTag: {
if (a.byteLength !== b.byteLength) {
return false;
}
return areObjectsEqual(new Uint8Array(a), new Uint8Array(b), stack);
}
case dataViewTag: {
if (a.byteLength !== b.byteLength || a.byteOffset !== b.byteOffset) {
return false;
}
return areObjectsEqual(a.buffer, b.buffer, stack);
}
case errorTag: {
return a.name === b.name && a.message === b.message;
}
case objectTag: {
if (a == null || b == null) {
return a === b;
}
const areEqualInstances =
areObjectsEqual(a.constructor, b.constructor, stack) ||
(isPlainObject(a) && isPlainObject(b));
if (!areEqualInstances) {
return false;
}
const aKeys = [...Object.keys(a), ...getSymbols(a)];
const bKeys = [...Object.keys(b), ...getSymbols(b)];
if (aKeys.length !== bKeys.length) {
return false;
}
for (let i = 0; i < aKeys.length; i++) {
const propKey = aKeys[i];
const aProp = (a as any)[propKey];
if (!(b as any).hasOwnProperty(propKey)) {
return false;
}
const bProp = (b as any)[propKey];
if (!areObjectsEqual(aProp, bProp, stack)) {
return false;
}
}
return true;
}
default: {
return false;
}
}
} finally {
stack.delete(a);
stack.delete(b);
}
}

108
yarn.lock
View File

@ -3352,24 +3352,24 @@ __metadata:
languageName: node
linkType: hard
"@vitest/expect@npm:2.0.2":
version: 2.0.2
resolution: "@vitest/expect@npm:2.0.2"
"@vitest/expect@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/expect@npm:2.0.5"
dependencies:
"@vitest/spy": "npm:2.0.2"
"@vitest/utils": "npm:2.0.2"
"@vitest/spy": "npm:2.0.5"
"@vitest/utils": "npm:2.0.5"
chai: "npm:^5.1.1"
tinyrainbow: "npm:^1.2.0"
checksum: 10c0/6f541f2f25244f41e9054699713ac9aedf1c82b82f6e0d4d4863565b352ff32794c2220f23603a01fc22b1eecbb9ea8e09eb2c93d80f7322c2b438a5e084ec08
checksum: 10c0/08cb1b0f106d16a5b60db733e3d436fa5eefc68571488eb570dfe4f599f214ab52e4342273b03dbe12331cc6c0cdc325ac6c94f651ad254cd62f3aa0e3d185aa
languageName: node
linkType: hard
"@vitest/pretty-format@npm:2.0.2, @vitest/pretty-format@npm:^2.0.2":
version: 2.0.2
resolution: "@vitest/pretty-format@npm:2.0.2"
"@vitest/pretty-format@npm:2.0.5, @vitest/pretty-format@npm:^2.0.5":
version: 2.0.5
resolution: "@vitest/pretty-format@npm:2.0.5"
dependencies:
tinyrainbow: "npm:^1.2.0"
checksum: 10c0/85749fae2ebcf7950c7a019a11b4272def00ee6568b6179e377e7374fb3b9ef6bd5bbef16c110b17881a3d1c772e315cb13a852758b08296b5a4bc665426952b
checksum: 10c0/236c0798c5170a0b5ad5d4bd06118533738e820b4dd30079d8fbcb15baee949d41c60f42a9f769906c4a5ce366d7ef11279546070646c0efc03128c220c31f37
languageName: node
linkType: hard
@ -3384,13 +3384,13 @@ __metadata:
languageName: node
linkType: hard
"@vitest/runner@npm:2.0.2":
version: 2.0.2
resolution: "@vitest/runner@npm:2.0.2"
"@vitest/runner@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/runner@npm:2.0.5"
dependencies:
"@vitest/utils": "npm:2.0.2"
"@vitest/utils": "npm:2.0.5"
pathe: "npm:^1.1.2"
checksum: 10c0/f4454b67f0c11318515ed6498cf8aa58ae18b6630a13d31201b55626c1c166dc07ceedd11ef373229595559724a6d3e8c961a8dfd8cc627c3b82bb38de0be40e
checksum: 10c0/d0ed3302a7e015bf44b7c0df9d8f7da163659e082d86f9406944b5a31a61ab9ddc1de530e06176d1f4ef0bde994b44bff4c7dab62aacdc235c8fc04b98e4a72a
languageName: node
linkType: hard
@ -3405,14 +3405,14 @@ __metadata:
languageName: node
linkType: hard
"@vitest/snapshot@npm:2.0.2":
version: 2.0.2
resolution: "@vitest/snapshot@npm:2.0.2"
"@vitest/snapshot@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/snapshot@npm:2.0.5"
dependencies:
"@vitest/pretty-format": "npm:2.0.2"
"@vitest/pretty-format": "npm:2.0.5"
magic-string: "npm:^0.30.10"
pathe: "npm:^1.1.2"
checksum: 10c0/7cb5e16d8a10ce71ec33cec57b191d28b82c4204b986a0ad04a956b401ae47d019f28a3680ee4256105bf9259f6df1208bff93fc4dc4b3e333a8c273f6b39200
checksum: 10c0/7bf38474248f5ae0aac6afad511785d2b7a023ac5158803c2868fd172b5b9c1a569fb1dd64a09a49e43fd342cab71ea485ada89b7f08d37b1622a5a0ac00271d
languageName: node
linkType: hard
@ -3425,12 +3425,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/spy@npm:2.0.2":
version: 2.0.2
resolution: "@vitest/spy@npm:2.0.2"
"@vitest/spy@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/spy@npm:2.0.5"
dependencies:
tinyspy: "npm:^3.0.0"
checksum: 10c0/7ef32945fc2a83add963da9baf35c6c2fa5b35afb03ab96fe1289ef5bbf3e85ef30bb6b80706f06935351ef10399551d37a993d58958260fe4b21f605a08c1a0
checksum: 10c0/70634c21921eb271b54d2986c21d7ab6896a31c0f4f1d266940c9bafb8ac36237846d6736638cbf18b958bd98e5261b158a6944352742accfde50b7818ff655e
languageName: node
linkType: hard
@ -3446,15 +3446,15 @@ __metadata:
languageName: node
linkType: hard
"@vitest/utils@npm:2.0.2":
version: 2.0.2
resolution: "@vitest/utils@npm:2.0.2"
"@vitest/utils@npm:2.0.5":
version: 2.0.5
resolution: "@vitest/utils@npm:2.0.5"
dependencies:
"@vitest/pretty-format": "npm:2.0.2"
"@vitest/pretty-format": "npm:2.0.5"
estree-walker: "npm:^3.0.3"
loupe: "npm:^3.1.1"
tinyrainbow: "npm:^1.2.0"
checksum: 10c0/d1f99ab1ea38ff36150405b2df390ec09cd0f014d05d3ae500c44bdc8746a14f34be3bf76c2a44c352b1c40d130bdf477c1043159ac62f2d63765aa1a8bb82a5
checksum: 10c0/0d1de748298f07a50281e1ba058b05dcd58da3280c14e6f016265e950bd79adab6b97822de8f0ea82d3070f585654801a9b1bcf26db4372e51cf7746bf86d73b
languageName: node
linkType: hard
@ -4207,7 +4207,7 @@ __metadata:
esbuild: "npm:0.23.0"
lodash: "npm:^4.17.21"
lodash-es: "npm:^4.17.21"
vitest: "npm:^2.0.2"
vitest: "npm:^2.0.5"
languageName: unknown
linkType: soft
@ -10482,9 +10482,9 @@ __metadata:
languageName: node
linkType: hard
"vite-node@npm:2.0.2":
version: 2.0.2
resolution: "vite-node@npm:2.0.2"
"vite-node@npm:2.0.5":
version: 2.0.5
resolution: "vite-node@npm:2.0.5"
dependencies:
cac: "npm:^6.7.14"
debug: "npm:^4.3.5"
@ -10493,7 +10493,7 @@ __metadata:
vite: "npm:^5.0.0"
bin:
vite-node: vite-node.mjs
checksum: 10c0/cf6fa40844134bd11d149ada94313ee2a47756ba7a98e143698b37c73b72c5850a9aaa799bd3076c09520e1be17079665846c763d3696b59359b88be77f299b6
checksum: 10c0/affcc58ae8d45bce3e8bc3b5767acd57c24441634e2cd967cf97f4e5ed2bcead1714b60150cdf7ee153ebad47659c5cd419883207e1a95b69790331e3243749f
languageName: node
linkType: hard
@ -10683,17 +10683,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^2.0.2":
version: 2.0.2
resolution: "vitest@npm:2.0.2"
"vitest@npm:^2.0.5":
version: 2.0.5
resolution: "vitest@npm:2.0.5"
dependencies:
"@ampproject/remapping": "npm:^2.3.0"
"@vitest/expect": "npm:2.0.2"
"@vitest/pretty-format": "npm:^2.0.2"
"@vitest/runner": "npm:2.0.2"
"@vitest/snapshot": "npm:2.0.2"
"@vitest/spy": "npm:2.0.2"
"@vitest/utils": "npm:2.0.2"
"@vitest/expect": "npm:2.0.5"
"@vitest/pretty-format": "npm:^2.0.5"
"@vitest/runner": "npm:2.0.5"
"@vitest/snapshot": "npm:2.0.5"
"@vitest/spy": "npm:2.0.5"
"@vitest/utils": "npm:2.0.5"
chai: "npm:^5.1.1"
debug: "npm:^4.3.5"
execa: "npm:^8.0.1"
@ -10704,13 +10704,13 @@ __metadata:
tinypool: "npm:^1.0.0"
tinyrainbow: "npm:^1.2.0"
vite: "npm:^5.0.0"
vite-node: "npm:2.0.2"
why-is-node-running: "npm:^2.2.2"
vite-node: "npm:2.0.5"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@types/node": ^18.0.0 || >=20.0.0
"@vitest/browser": 2.0.2
"@vitest/ui": 2.0.2
"@vitest/browser": 2.0.5
"@vitest/ui": 2.0.5
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@ -10728,7 +10728,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10c0/4ef4d8d5a32ee91f34715b8ae1895062df9ca36be5a88ff916ee4bb2a5e01dfa3f105031f7a5939c98f6401b3a6b2bfd1de6caab0ac84cca0e2df364a19c3526
checksum: 10c0/b4e6cca00816bf967a8589111ded72faa12f92f94ccdd0dcd0698ffcfdfc52ec662753f66b387549c600ac699b993fd952efbd99dc57fcf4d1c69a2f1022b259
languageName: node
linkType: hard
@ -10923,6 +10923,18 @@ __metadata:
languageName: node
linkType: hard
"why-is-node-running@npm:^2.3.0":
version: 2.3.0
resolution: "why-is-node-running@npm:2.3.0"
dependencies:
siginfo: "npm:^2.0.0"
stackback: "npm:0.0.2"
bin:
why-is-node-running: cli.js
checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054
languageName: node
linkType: hard
"win-release@npm:^1.0.0":
version: 1.1.1
resolution: "win-release@npm:1.1.1"