Skip to content
71 changes: 71 additions & 0 deletions src/interpreter/deep-equal.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +12 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

愚直にやるなら、indexAとindexBが同時に見つかるのが二回確認できればその先の一致が保証できると思います(一回だとループの開始位置にズレがある場合があるが、二回なら一回目のループ終了地点で開始を固定できる)

Copy link
Contributor Author

@marihachi marihachi Mar 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aとbで同じ位置で循環が検出される場合は良いと思っています。
aとbでそれぞれ別の位置を起点として循環が現れるが、パターンとしては一致しているor一致してないというのを判定する処理が必要な気がしています。
具体的な処理は思いついていません。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ん、2回まわすってことですか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい。同位置で循環があった地点からもう一度回せば、「aとbで同じ位置で循環が検出される場合」に持ち込めるのでうまくいくはずです。(だいぶ効率が悪いとは思いますが)


// 次の参照パスを生成
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<string, unknown>)[key], (b as Record<string, unknown>)[key], nextRefsA, nextRefsB)) return false;
}
return true;
}

return false;
}
92 changes: 92 additions & 0 deletions test/deep-equal.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});