diff --git a/frontends/web/src/utils/equal.js b/frontends/web/src/utils/equal.js deleted file mode 100644 index 1e4cd8dce6..0000000000 --- a/frontends/web/src/utils/equal.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright 2018 Shift Devices AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const isArray = Array.isArray; -const keyList = Object.keys; -const hasProp = Object.prototype.hasOwnProperty; - -export function equal(a, b) { - if (Object.is(a, b)) { - return true; - } - - if (a && b && typeof a === 'object' && typeof b === 'object') { - let arrA = isArray(a), arrB = isArray(b), i, length, key; - - if (arrA && arrB) { - length = a.length; - if (length !== b.length) { - return false; - } - for (i = 0; i < length; i++) { - if (!equal(a[i], b[i])) { - return false; - } - } - return true; - } - - if (arrA !== arrB) { - return false; - } - - let keys = keyList(a); - length = keys.length; - - if (length !== keyList(b).length) { - return false; - } - - for (i = 0; i < length; i++) { - if (!hasProp.call(b, keys[i])) { - return false; - } - } - - for (i = 0; i < length; i++) { - key = keys[i]; - if (!equal(a[key], b[key])) { - return false; - } - } - - return true; - } - - return false; -} diff --git a/frontends/web/src/utils/equal.test.tsx b/frontends/web/src/utils/equal.test.tsx index 8a5be45213..6c384cfe37 100644 --- a/frontends/web/src/utils/equal.test.tsx +++ b/frontends/web/src/utils/equal.test.tsx @@ -114,5 +114,33 @@ describe('equal', () => { expect(equal(a, null)).toBeFalsy(); expect(equal(null, a)).toBeFalsy(); }); + + it('deep compares nested structures', () => { + const a = { foo: [1, { bar: 'baz' }] }; + const b = { foo: [1, { bar: 'baz' }] }; + expect(equal(a, b)).toBeTruthy(); + }); + }); + + describe('RegExp, functions and dates are currently not supported', () => { + + it('compares RegExp objects correctly', () => { + expect(equal(/foo/g, /foo/g)).toBeTruthy(); + expect(equal(/foo/g, /bar/g)).toBeFalsy(); + }); + + it('compares Date objects correctly', () => { + expect(equal(new Date('2020-01-01'), new Date('2020-01-01'))).toBeTruthy(); + expect(equal(new Date('2020-01-01'), new Date('2021-01-01'))).toBeFalsy(); + }); + + it('does not consider functions equal', () => { + const a = () => {}; + const b = () => {}; + expect(equal(a, b)).toBeFalsy(); + }); + + }); + }); diff --git a/frontends/web/src/utils/equal.ts b/frontends/web/src/utils/equal.ts new file mode 100644 index 0000000000..ea59b3caa4 --- /dev/null +++ b/frontends/web/src/utils/equal.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2018 Shift Devices AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const isArray = Array.isArray; +const hasProp = Object.prototype.hasOwnProperty; +const typedKeys = (obj: Readonly): readonly (keyof T)[] => { + return Object.keys(obj) as (keyof T)[]; +}; + +/** + * Performs a deep equality check between two values. + * + * This function compares primitive types, arrays, plain objects, Date instances, + * and RegExp objects. It returns true if the values are deeply equal, false otherwise. + * + * - Uses `Object.is` for primitive comparison (handles `NaN`, `-0`, etc.) + * - Recursively checks array contents and object properties + * - Properly compares Date and RegExp objects + * - Returns false for functions, symbols, maps, sets, or class instances (not handled) + * + * @param a - The first value to compare. + * @param b - The second value to compare. + * @returns `true` if values are deeply equal, `false` otherwise. + */ +export const equal = (a: unknown, b: unknown): boolean => { + if (Object.is(a, b)) { + return true; + } + + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + + if (a instanceof RegExp && b instanceof RegExp) { + return a.toString() === b.toString(); + } + + if ( + (a instanceof Date) !== (b instanceof Date) + || (a instanceof RegExp) !== (b instanceof RegExp) + ) { + return false; + } + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (isArray(a) && isArray(b)) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!equal(a[i], b[i])) { + return false; + } + } + return true; + } + + if (isArray(a) !== isArray(b)) { + return false; + } + + const aKeys = typedKeys(a); + const bKeys = typedKeys(b); + + if (aKeys.length !== bKeys.length) { + return false; + } + + for (const key of aKeys) { + if (!hasProp.call(b, key)) { + return false; + } + if (!equal(a[key], b[key])) { + return false; + } + } + + return true; + } + + return false; +};