From 2c85ece89854280bb368e5306be30c8a62035b7b Mon Sep 17 00:00:00 2001 From: Tony Quetano Date: Fri, 29 Apr 2022 06:25:39 -0700 Subject: [PATCH 1/5] add "exotic object" comparison support --- .eslintrc | 1 - __tests__/__helpers__/testSuites.js | 49 ++++++++++++++++++ src/comparator.ts | 30 +++++++---- src/types.ts | 24 +++++++++ src/utils.ts | 78 ++++++++++++++++++++++------- 5 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 src/types.ts diff --git a/.eslintrc b/.eslintrc index d48d448..1afa2a8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,4 @@ { - "extends": ["airbnb"], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "settings": { diff --git a/__tests__/__helpers__/testSuites.js b/__tests__/__helpers__/testSuites.js index 8a9bc6d..f743d5f 100644 --- a/__tests__/__helpers__/testSuites.js +++ b/__tests__/__helpers__/testSuites.js @@ -49,6 +49,20 @@ module.exports = [ value1: -Infinity, value2: Infinity, }, + { + deepEqual: true, + description: 'equal number objects', + shallowEqual: true, + value1: new Number(1), + value2: new Number(1), + }, + { + deepEqual: false, + description: 'not equal number objects', + shallowEqual: false, + value1: new Number(1), + value2: new Number(2), + }, { deepEqual: false, description: 'number and array are not equal', @@ -84,6 +98,20 @@ module.exports = [ value1: 'a', value2: 'b', }, + { + deepEqual: true, + description: 'equal string objects', + shallowEqual: true, + value1: new String('foo'), + value2: new String('foo'), + }, + { + deepEqual: false, + description: 'not equal string objects', + shallowEqual: false, + value1: new String('foo'), + value2: new String('bar'), + }, { deepEqual: false, description: 'empty string and null are not equal', @@ -112,6 +140,20 @@ module.exports = [ value1: false, value2: false, }, + { + deepEqual: true, + description: 'equal boolean objects (true)', + shallowEqual: true, + value1: new Boolean(true), + value2: new Boolean(true), + }, + { + deepEqual: true, + description: 'equal boolean objects (false)', + shallowEqual: true, + value1: new Boolean(false), + value2: new Boolean(false), + }, { deepEqual: false, description: 'not equal booleans', @@ -119,6 +161,13 @@ module.exports = [ value1: true, value2: false, }, + { + deepEqual: false, + description: 'not equal boolean objects', + shallowEqual: false, + value1: new Boolean(true), + value2: new Boolean(false), + }, { deepEqual: false, description: '1 and true are not equal', diff --git a/src/comparator.ts b/src/comparator.ts index e436c46..3a15f20 100644 --- a/src/comparator.ts +++ b/src/comparator.ts @@ -1,7 +1,7 @@ import { - EqualityComparator, - InternalEqualityComparator, areArraysEqual, + areDatesEqual, + areExoticObjectsEqual, areMapsEqual, areObjectsEqual, areRegExpsEqual, @@ -11,17 +11,31 @@ import { sameValueZeroEqual, } from './utils'; +import type { EqualityComparator, InternalEqualityComparator } from './types'; + const HAS_MAP_SUPPORT = typeof Map === 'function'; const HAS_SET_SUPPORT = typeof Set === 'function'; -export type EqualityComparatorCreator = (fn: EqualityComparator) => InternalEqualityComparator; +export type EqualityComparatorCreator = ( + fn: EqualityComparator, +) => InternalEqualityComparator; -export function createComparator(createIsEqual?: EqualityComparatorCreator): EqualityComparator { +export function createComparator( + createIsEqual?: EqualityComparatorCreator, +): EqualityComparator { const isEqual: InternalEqualityComparator = /* eslint-disable no-use-before-define */ typeof createIsEqual === 'function' ? createIsEqual(comparator) - : (a: any, b: any, indexOrKeyA: any, indexOrKeyB: any, parentA: any, parentB: any, meta: any) => comparator(a, b, meta); + : ( + a: any, + b: any, + indexOrKeyA: any, + indexOrKeyB: any, + parentA: any, + parentB: any, + meta: any, + ) => comparator(a, b, meta); /* eslint-enable */ /** @@ -53,9 +67,7 @@ export function createComparator(createIsEqual?: EqualityComparatorCreator): Equ bShape = b instanceof Date; if (aShape || bShape) { - return ( - aShape === bShape && sameValueZeroEqual(a.getTime(), b.getTime()) - ); + return aShape === bShape && areDatesEqual(a, b); } aShape = a instanceof RegExp; @@ -87,7 +99,7 @@ export function createComparator(createIsEqual?: EqualityComparatorCreator): Equ } } - return areObjectsEqual(a, b, isEqual, meta); + return areExoticObjectsEqual(a, b, isEqual, meta); } return a !== a && b !== b; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..336f1c3 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,24 @@ +export type Primitive = bigint | boolean | number | string | symbol; + +export type InternalEqualityComparator = ( + objectA: any, + objectB: any, + indexOrKeyA: any, + indexOrKeyB: any, + parentA: any, + parentB: any, + meta: any, +) => boolean; + +export type EqualityComparator = ( + objectA: A, + objectB: B, + meta?: Meta, +) => boolean; + +export type ExoticEqualityComparator = ( + objectA: any, + objectB: any, + comparator: InternalEqualityComparator, + meta: any, +) => boolean; diff --git a/src/utils.ts b/src/utils.ts index d48123f..eff4dba 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,28 +1,36 @@ -const HAS_WEAKSET_SUPPORT = typeof WeakSet === 'function'; +import type { + EqualityComparator, + ExoticEqualityComparator, + InternalEqualityComparator, + Primitive, +} from './types'; const { keys } = Object; +const { toString } = Object.prototype; + +const HAS_WEAKSET_SUPPORT = typeof WeakSet === 'function'; + +const EXOTIC_OBJECT_COMPARATORS: Record = { + '[object BigInt]': areNonNumberPrimitiveWrappersEqual, + '[object Boolean]': areNonNumberPrimitiveWrappersEqual, + '[object Date]': areDatesEqual, + '[object Function]': strictEqual, + '[object Map]': areMapsEqual, + '[object Number]': areNumberWrappersEqual, + '[object Promise]': strictEqual, + '[object RegExp]': areRegExpsEqual, + '[object Set]': areSetsEqual, + '[object String]': areNonNumberPrimitiveWrappersEqual, + '[object Symbol]': strictEqual, + '[object WeakMap]': strictEqual, + '[object WeakSet]': strictEqual, +}; type Cache = { add: (value: any) => void; has: (value: any) => boolean; }; -export type InternalEqualityComparator = ( - objectA: any, - objectB: any, - indexOrKeyA: any, - indexOrKeyB: any, - parentA: any, - parentB: any, - meta: any, -) => boolean; - -export type EqualityComparator = ( - objectA: A, - objectB: B, - meta?: Meta, -) => boolean; - /** * are the values passed strictly equal or both NaN * @@ -34,6 +42,10 @@ export function sameValueZeroEqual(a: any, b: any) { return a === b || (a !== a && b !== b); } +function strictEqual(a: any, b: any) { + return a === b; +} + /** * is the value a plain object * @@ -175,6 +187,38 @@ export function areArraysEqual( return true; } +export function areDatesEqual(a: Date, b: Date) { + return sameValueZeroEqual(a.getTime(), b.getTime()); +} + +function areNonNumberPrimitiveWrappersEqual( + a: Omit, + b: Omit, +) { + return a.valueOf() === b.valueOf(); +} + +function areNumberWrappersEqual(a: number, b: number) { + return sameValueZeroEqual(a.valueOf(), b.valueOf()); +} + +export function areExoticObjectsEqual( + a: any, + b: any, + isEqual: InternalEqualityComparator, + meta: any, +) { + const aType = toString.call(a); + + if (aType !== toString.call(b)) { + return false; + } + + const comparator = EXOTIC_OBJECT_COMPARATORS[aType] || areObjectsEqual; + + return comparator(a, b, isEqual, meta); +} + /** * are the maps equal in value * From aab713048b513c868936f92563aafd5491f9479b Mon Sep 17 00:00:00 2001 From: Tony Quetano Date: Fri, 29 Apr 2022 06:46:06 -0700 Subject: [PATCH 2/5] revert to simple `valueOf` comparison --- src/comparator.ts | 14 +++++++++---- src/types.ts | 7 ------- src/utils.ts | 51 +---------------------------------------------- 3 files changed, 11 insertions(+), 61 deletions(-) diff --git a/src/comparator.ts b/src/comparator.ts index 3a15f20..162876e 100644 --- a/src/comparator.ts +++ b/src/comparator.ts @@ -1,7 +1,5 @@ import { areArraysEqual, - areDatesEqual, - areExoticObjectsEqual, areMapsEqual, areObjectsEqual, areRegExpsEqual, @@ -16,6 +14,8 @@ import type { EqualityComparator, InternalEqualityComparator } from './types'; const HAS_MAP_SUPPORT = typeof Map === 'function'; const HAS_SET_SUPPORT = typeof Set === 'function'; +const { valueOf } = Object.prototype; + export type EqualityComparatorCreator = ( fn: EqualityComparator, ) => InternalEqualityComparator; @@ -67,7 +67,9 @@ export function createComparator( bShape = b instanceof Date; if (aShape || bShape) { - return aShape === bShape && areDatesEqual(a, b); + return ( + aShape === bShape && sameValueZeroEqual(a.getTime(), b.getTime()) + ); } aShape = a instanceof RegExp; @@ -99,7 +101,11 @@ export function createComparator( } } - return areExoticObjectsEqual(a, b, isEqual, meta); + if (a.valueOf !== valueOf || b.valueOf !== valueOf) { + return sameValueZeroEqual(a.valueOf(), b.valueOf()); + } + + return areObjectsEqual(a, b, isEqual, meta); } return a !== a && b !== b; diff --git a/src/types.ts b/src/types.ts index 336f1c3..33a019e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,10 +15,3 @@ export type EqualityComparator = ( objectB: B, meta?: Meta, ) => boolean; - -export type ExoticEqualityComparator = ( - objectA: any, - objectB: any, - comparator: InternalEqualityComparator, - meta: any, -) => boolean; diff --git a/src/utils.ts b/src/utils.ts index eff4dba..c790fdf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,31 +1,14 @@ import type { EqualityComparator, - ExoticEqualityComparator, InternalEqualityComparator, Primitive, } from './types'; const { keys } = Object; -const { toString } = Object.prototype; +const { toString, valueOf } = Object.prototype; const HAS_WEAKSET_SUPPORT = typeof WeakSet === 'function'; -const EXOTIC_OBJECT_COMPARATORS: Record = { - '[object BigInt]': areNonNumberPrimitiveWrappersEqual, - '[object Boolean]': areNonNumberPrimitiveWrappersEqual, - '[object Date]': areDatesEqual, - '[object Function]': strictEqual, - '[object Map]': areMapsEqual, - '[object Number]': areNumberWrappersEqual, - '[object Promise]': strictEqual, - '[object RegExp]': areRegExpsEqual, - '[object Set]': areSetsEqual, - '[object String]': areNonNumberPrimitiveWrappersEqual, - '[object Symbol]': strictEqual, - '[object WeakMap]': strictEqual, - '[object WeakSet]': strictEqual, -}; - type Cache = { add: (value: any) => void; has: (value: any) => boolean; @@ -187,38 +170,6 @@ export function areArraysEqual( return true; } -export function areDatesEqual(a: Date, b: Date) { - return sameValueZeroEqual(a.getTime(), b.getTime()); -} - -function areNonNumberPrimitiveWrappersEqual( - a: Omit, - b: Omit, -) { - return a.valueOf() === b.valueOf(); -} - -function areNumberWrappersEqual(a: number, b: number) { - return sameValueZeroEqual(a.valueOf(), b.valueOf()); -} - -export function areExoticObjectsEqual( - a: any, - b: any, - isEqual: InternalEqualityComparator, - meta: any, -) { - const aType = toString.call(a); - - if (aType !== toString.call(b)) { - return false; - } - - const comparator = EXOTIC_OBJECT_COMPARATORS[aType] || areObjectsEqual; - - return comparator(a, b, isEqual, meta); -} - /** * are the maps equal in value * From 1a326e4aa3400046ee725d4390b15a9327d71815 Mon Sep 17 00:00:00 2001 From: Tony Quetano Date: Fri, 29 Apr 2022 06:50:36 -0700 Subject: [PATCH 3/5] remove `strictEqual` method used in debugging --- src/utils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index c790fdf..2e4d6e9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,10 +25,6 @@ export function sameValueZeroEqual(a: any, b: any) { return a === b || (a !== a && b !== b); } -function strictEqual(a: any, b: any) { - return a === b; -} - /** * is the value a plain object * From 074afa8f986fe056492b7c59f59227724720ffae Mon Sep 17 00:00:00 2001 From: Tony Quetano Date: Fri, 29 Apr 2022 06:51:09 -0700 Subject: [PATCH 4/5] remove destructured local values used in debugging --- src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 2e4d6e9..33d6a7e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,7 +5,6 @@ import type { } from './types'; const { keys } = Object; -const { toString, valueOf } = Object.prototype; const HAS_WEAKSET_SUPPORT = typeof WeakSet === 'function'; From 761da593ba2969fe904d4d637629ab42ec1c0679 Mon Sep 17 00:00:00 2001 From: Tony Quetano Date: Fri, 29 Apr 2022 06:52:18 -0700 Subject: [PATCH 5/5] remove unused `Primitive` type --- src/types.ts | 2 -- src/utils.ts | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/types.ts b/src/types.ts index 33a019e..8e31e6b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,3 @@ -export type Primitive = bigint | boolean | number | string | symbol; - export type InternalEqualityComparator = ( objectA: any, objectB: any, diff --git a/src/utils.ts b/src/utils.ts index 33d6a7e..3d05aef 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,4 @@ -import type { - EqualityComparator, - InternalEqualityComparator, - Primitive, -} from './types'; +import type { EqualityComparator, InternalEqualityComparator } from './types'; const { keys } = Object;