diff --git a/.eslintrc.json b/.eslintrc.json index 2f8306f7..3723d97d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -110,6 +110,7 @@ "escapeRegExp": "readonly", "deferPromise": "readonly", "clone": "readonly", + "deepEqual": "readonly", "generateId": "readonly", "promiseAnimationFrame": "readonly", "DynamicProperty": "readonly", diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 81330893..72ab7474 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -176,6 +176,72 @@ const clone = (() => { return clone; })(); +const deepEqual = (() => { + // eslint-disable-next-line no-shadow + function deepEqual(value1, value2) { + if (value1 === value2) { return true; } + + const type = typeof value1; + if (typeof value2 !== type) { return false; } + + switch (type) { + case 'object': + case 'function': + return deepEqualInternal(value1, value2, new Set()); + default: + return false; + } + } + + function deepEqualInternal(value1, value2, visited1) { + if (value1 === value2) { return true; } + + const type = typeof value1; + if (typeof value2 !== type) { return false; } + + switch (type) { + case 'object': + case 'function': + { + if (value1 === null || value2 === null) { return false; } + const array = Array.isArray(value1); + if (array !== Array.isArray(value2)) { return false; } + if (visited1.has(value1)) { return false; } + visited1.add(value1); + return array ? areArraysEqual(value1, value2, visited1) : areObjectsEqual(value1, value2, visited1); + } + default: + return false; + } + } + + function areObjectsEqual(value1, value2, visited1) { + const keys1 = Object.keys(value1); + const keys2 = Object.keys(value2); + if (keys1.length !== keys2.length) { return false; } + + const keys1Set = new Set(keys1); + for (const key of keys2) { + if (!keys1Set.has(key) || !deepEqualInternal(value1[key], value2[key], visited1)) { return false; } + } + + return true; + } + + function areArraysEqual(value1, value2, visited1) { + const length = value1.length; + if (length !== value2.length) { return false; } + + for (let i = 0; i < length; ++i) { + if (!deepEqualInternal(value1[i], value2[i], visited1)) { return false; } + } + + return true; + } + + return deepEqual; +})(); + function generateId(length) { const array = new Uint8Array(length); crypto.getRandomValues(array); diff --git a/test/test-core.js b/test/test-core.js index 3e8b8414..2b29b7f1 100644 --- a/test/test-core.js +++ b/test/test-core.js @@ -32,7 +32,7 @@ const vm = new VM({ vm.execute([ 'mixed/js/core.js' ]); -const [DynamicProperty] = vm.get(['DynamicProperty']); +const [DynamicProperty, deepEqual] = vm.get(['DynamicProperty', 'deepEqual']); function testDynamicProperty() { @@ -161,9 +161,139 @@ function testDynamicProperty() { } } +function testDeepEqual() { + const data = [ + // Simple tests + { + value1: 0, + value2: 0, + expected: true + }, + { + value1: null, + value2: null, + expected: true + }, + { + value1: 'test', + value2: 'test', + expected: true + }, + { + value1: true, + value2: true, + expected: true + }, + { + value1: 0, + value2: 1, + expected: false + }, + { + value1: null, + value2: false, + expected: false + }, + { + value1: 'test1', + value2: 'test2', + expected: false + }, + { + value1: true, + value2: false, + expected: false + }, + + // Simple object tests + { + value1: {}, + value2: {}, + expected: true + }, + { + value1: {}, + value2: [], + expected: false + }, + { + value1: [], + value2: [], + expected: true + }, + { + value1: {}, + value2: null, + expected: false + }, + + // Complex object tests + { + value1: [1], + value2: [], + expected: false + }, + { + value1: [1], + value2: [1], + expected: true + }, + { + value1: [1], + value2: [2], + expected: false + }, + + { + value1: {}, + value2: {test: 1}, + expected: false + }, + { + value1: {test: 1}, + value2: {test: 1}, + expected: true + }, + { + value1: {test: 1}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: true}}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: [true]}}, + value2: {test: {test2: [true]}}, + expected: true + }, + + // Recursive + { + value1: (() => { const x = {}; x.x = x; return x; })(), + value2: (() => { const x = {}; x.x = x; return x; })(), + expected: false + } + ]; + + let index = 0; + for (const {value1, value2, expected} of data) { + const actual1 = deepEqual(value1, value2); + assert.strictEqual(actual1, expected, `Failed for test ${index}`); + + const actual2 = deepEqual(value2, value1); + assert.strictEqual(actual2, expected, `Failed for test ${index}`); + + ++index; + } +} + function main() { testDynamicProperty(); + testDeepEqual(); }