diff --git a/src/interpreter/deep-equal.ts b/src/interpreter/deep-equal.ts new file mode 100644 index 00000000..c3fc8b13 --- /dev/null +++ b/src/interpreter/deep-equal.ts @@ -0,0 +1,71 @@ +export function deepEqual(a: unknown, b: unknown): boolean { + return deepEqualRefs(a, b, [], []); +} + +function deepEqualRefs(a: unknown, b: unknown, refsA: unknown[], refsB: unknown[]): boolean { + // プリミティブ値や参照の比較 + // NOTE: Object.is()はNaN同士の比較でもtrue + if (Object.is(a, b)) return true; + + // Object (a、b共にnullは含まない) + if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') { + // 参照の循環をチェック + // 両方の循環が確認された時点で、その先も一致すると保証できるためtrueで返す + const indexA = refsA.findIndex(x => x === a); + const indexB = refsB.findIndex(x => x === b); + if (indexA !== -1 && indexB !== -1) { + return true; + } + + // 次の参照パスを生成 + const nextRefsA = [...refsA, a]; + const nextRefsB = [...refsB, b]; + + // Array + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepEqualRefs(a[i], b[i], nextRefsA, nextRefsB)) return false; + } + return true; + } + + // Map + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) return false; + const aEntries = a.entries(); + const bEntries = b.entries(); + for (let i = 0; i < a.size; i++) { + const entryA = aEntries.next(); + const entryB = bEntries.next(); + if (!deepEqualRefs(entryA.value[0], entryB.value[0], nextRefsA, nextRefsB)) return false; + if (!deepEqualRefs(entryA.value[1], entryB.value[1], nextRefsA, nextRefsB)) return false; + } + return true; + } + + // Set + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) return false; + const aValues = a.values(); + const bValues = b.values(); + for (let i = 0; i < a.size; i++) { + const valueA = aValues.next(); + const valueB = bValues.next(); + if (!deepEqualRefs(valueA.value, valueB.value, nextRefsA, nextRefsB)) return false; + } + return true; + } + + // object keys + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + for (const key of keysA) { + if (!deepEqualRefs((a as Record)[key], (b as Record)[key], nextRefsA, nextRefsB)) return false; + } + return true; + } + + return false; +} diff --git a/test/deep-equal.ts b/test/deep-equal.ts new file mode 100644 index 00000000..f84ee3b6 --- /dev/null +++ b/test/deep-equal.ts @@ -0,0 +1,92 @@ +import * as assert from 'assert'; +import { deepEqual } from '../src/interpreter/deep-equal'; + +describe('compare', () => { + test('object and object', () => { + assert.strictEqual(deepEqual({ a: 1 }, { a: 1 }), true); + assert.strictEqual(deepEqual({ a: 1 }, { a: 2 }), false); + assert.strictEqual(deepEqual({ a: 1 }, { a: 1, b: 2 }), false); + }); + + test('number and number', () => { + assert.strictEqual(deepEqual(1, 1), true); + assert.strictEqual(deepEqual(1, 2), false); + }); + + test('number[] and number[]', () => { + assert.strictEqual(deepEqual([1, 2, 3], [1, 2, 3]), true); + assert.strictEqual(deepEqual([1, 2, 3], [4, 5, 6]), false); + assert.strictEqual(deepEqual([1, 2], [1, 2, 3]), false); + assert.strictEqual(deepEqual([1, 2, 3], [1, 2]), false); + }); + + test('string[] and string[]', () => { + assert.strictEqual(deepEqual(['a', 'b', 'c'], ['a', 'b', 'c']), true); + assert.strictEqual(deepEqual(['a', 'b', 'c'], ['x', 'y', 'z']), false); + assert.strictEqual(deepEqual(['a', 'b'], ['a', 'b', 'c']), false); + assert.strictEqual(deepEqual(['a', 'b', 'c'], ['a', 'b']), false); + }); + + test('object and null', () => { + assert.strictEqual(deepEqual({ a: 1 }, null), false); + }); +}); + +test('null, undefined, NaN', () => { + assert.strictEqual(deepEqual(null, null), true); + assert.strictEqual(deepEqual(undefined, undefined), true); + assert.strictEqual(deepEqual(NaN, NaN), true); + assert.strictEqual(deepEqual(null, undefined), false); + assert.strictEqual(deepEqual(null, NaN), false); + assert.strictEqual(deepEqual(undefined, NaN), false); +}); + +describe('recursive', () => { + test('simple', () => { + let x: any = { n: null }; + x.n = x; + let y: any = { n: null }; + y.n = y; + assert.strictEqual(deepEqual(x, y), true); + }); + + test('object', () => { + let x: any = { a: { b: { a: null } } }; + x.a.b.a = x.a; + let y: any = { a: { b: null } }; + y.a.b = y; + assert.strictEqual(deepEqual(x, y), true); + }); + + test('object 2', () => { + let x: any = { a: { b: { a: null } } }; + x.a.b.a = x.a; + let y: any = { a: { b: null } }; + y.a.b = y.a; + assert.strictEqual(deepEqual(x, y), false); + }); + + test('different path of object', () => { + let x: any = { a: { b: null } }; + x.a.b = x; + let y: any = { a: { b: { a: { b: { a: null } } } } }; + y.a.b.a.b.a = y.a.b.a; + assert.strictEqual(deepEqual(x, y), true); + }); + + test('different path of object 2', () => { + let x: any = { a: { b: null } }; + x.a.b = x; + let y: any = { a: { b: { a: { b: { a: null } } } } }; + y.a.b.a.b.a = y.a.b; + assert.strictEqual(deepEqual(x, y), false); + }); + + test('object and array', () => { + let a: any = [{ a: [] }]; + let b: any = [{ a: [] }]; + a[0].a[0] = a; + b[0].a[0] = b[0]; + assert.strictEqual(deepEqual(a, b), false); + }); +});