import * as Equal from "effect/Equal" import * as Hash from "effect/Hash" import * as HashMap from "effect/HashMap" import * as Option from "effect/Option" import { describe, expect, it } from "vitest" describe("Equal.equals", () => { describe("plain objects", () => { it("should return true for structurally identical objects (structural equality)", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return true for same reference", () => { const obj = { a: 1, b: 2 } expect(Equal.equals(obj, obj)).toBe(true) }) it("should return true for empty objects", () => { expect(Equal.equals({}, {})).toBe(true) }) it("should return false for objects with different values", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 3 } expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should return false for objects with different keys", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, c: 2 } expect(Equal.equals(obj1, obj2)).toBe(false) }) describe("symbol properties", () => { it("should return true for objects with identical symbol keys and values", () => { const sym1 = Symbol("test") const sym2 = Symbol("other") const obj1 = { [sym1]: 1, [sym2]: 2 } const obj2 = { [sym1]: 1, [sym2]: 2 } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return false for objects with different symbol keys", () => { const sym1 = Symbol("test") const sym2 = Symbol("test") // Different symbol with same description const obj1 = { [sym1]: 1 } const obj2 = { [sym2]: 1 } expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should return false for objects with different symbol values", () => { const sym = Symbol("test") const obj1 = { [sym]: 1 } const obj2 = { [sym]: 2 } expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should return true for objects with mixed string and symbol keys", () => { const sym = Symbol("test") const obj1 = { a: 1, [sym]: 2 } const obj2 = { a: 1, [sym]: 2 } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return false when one object has symbol keys and the other doesn't", () => { const sym = Symbol("test") const obj1 = { a: 1, [sym]: 2 } const obj2 = { a: 1 } expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should consider non-enumerable symbol properties", () => { const sym1 = Symbol("enumerable") const sym2 = Symbol("non-enumerable") const obj1: any = { a: 1 } const obj2: any = { a: 1 } // Add enumerable symbol property obj1[sym1] = "visible" obj2[sym1] = "visible" // Add non-enumerable symbol property with same value Object.defineProperty(obj1, sym2, { value: "hidden", enumerable: false }) Object.defineProperty(obj2, sym2, { value: "hidden", enumerable: false }) expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should consider non-enumerable symbol properties even when they differ", () => { const sym1 = Symbol("enumerable") const sym2 = Symbol("non-enumerable") const obj1: any = { a: 1 } const obj2: any = { a: 1 } // Add same enumerable symbol property obj1[sym1] = "visible" obj2[sym1] = "visible" // Add non-enumerable symbol properties with different values Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false }) Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false }) expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should consider enumerable symbol properties even when string properties are the same", () => { const sym = Symbol("test") const obj1: any = { a: 1, b: 2 } const obj2: any = { a: 1, b: 2 } obj1[sym] = "different" obj2[sym] = "values" expect(Equal.equals(obj1, obj2)).toBe(false) }) }) describe("non-enumerable properties", () => { it("should handle non-enumerable string properties correctly", () => { const obj1 = { a: 1 } const obj2 = { a: 1 } Object.defineProperty(obj1, "hidden", { value: "secret", enumerable: false }) Object.defineProperty(obj2, "hidden", { value: "secret", enumerable: false }) expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should consider non-enumerable string properties even when they differ", () => { const obj1 = { a: 1 } const obj2 = { a: 1 } Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false }) Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false }) expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should consider enumerable properties and non-enumerable ones", () => { const obj1: any = { a: 1 } const obj2: any = { a: 1 } // Add same enumerable property obj1.b = 2 obj2.b = 2 // Add different non-enumerable properties (should be considered) Object.defineProperty(obj1, "hidden", { value: "secret1", enumerable: false }) Object.defineProperty(obj2, "hidden", { value: "secret2", enumerable: false }) expect(Equal.equals(obj1, obj2)).toBe(false) }) }) describe("hash consistency with symbol properties", () => { it("should produce same hash for objects with identical symbol keys and values", () => { const sym1 = Symbol("test") const sym2 = Symbol("other") const obj1 = { [sym1]: 1, [sym2]: 2 } const obj2 = { [sym1]: 1, [sym2]: 2 } expect(Hash.hash(obj1)).toBe(Hash.hash(obj2)) expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return false for equality even if symbols have same hash", () => { const sym1 = Symbol("test") const sym2 = Symbol("test") // Different symbol with same description const obj1 = { [sym1]: 1 } const obj2 = { [sym2]: 1 } // Note: symbols with same description may have same hash (acceptable hash collision) // but equality should still work correctly expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should produce different hash for symbols with different descriptions", () => { const sym1 = Symbol("first") const sym2 = Symbol("second") const obj1 = { [sym1]: 1 } const obj2 = { [sym2]: 1 } expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2)) expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should produce different hash for objects with different symbol values", () => { const sym = Symbol("test") const obj1 = { [sym]: 1 } const obj2 = { [sym]: 2 } expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2)) expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should produce same hash for objects with mixed string and symbol keys", () => { const sym = Symbol("test") const obj1 = { a: 1, [sym]: 2 } const obj2 = { a: 1, [sym]: 2 } expect(Hash.hash(obj1)).toBe(Hash.hash(obj2)) expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should consider non-enumerable symbol properties in hash calculation", () => { const sym1 = Symbol("enumerable") const sym2 = Symbol("non-enumerable") const obj1: any = { a: 1 } const obj2: any = { a: 1 } // Add same enumerable symbol property obj1[sym1] = "visible" obj2[sym1] = "visible" // Add different non-enumerable symbol properties Object.defineProperty(obj1, sym2, { value: "hidden1", enumerable: false }) Object.defineProperty(obj2, sym2, { value: "hidden2", enumerable: false }) expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2)) expect(Equal.equals(obj1, obj2)).toBe(false) }) }) }) describe("plain arrays", () => { it("should return true for structurally identical arrays (structural equality)", () => { const arr1 = [1, 2, 3] const arr2 = [1, 2, 3] expect(Equal.equals(arr1, arr2)).toBe(true) }) it("should return true for same reference", () => { const arr = [1, 2, 3] expect(Equal.equals(arr, arr)).toBe(true) }) it("should return true for empty arrays", () => { expect(Equal.equals([], [])).toBe(true) }) it("should return false for arrays with different elements", () => { const arr1 = [1, 2, 3] const arr2 = [1, 2, 4] expect(Equal.equals(arr1, arr2)).toBe(false) }) it("should return false for arrays with different lengths", () => { const arr1 = [1, 2, 3] const arr2 = [1, 2] expect(Equal.equals(arr1, arr2)).toBe(false) }) }) describe("primitives", () => { it("should work correctly for numbers", () => { expect(Equal.equals(42, 42)).toBe(true) expect(Equal.equals(42, 43)).toBe(false) }) it("should work correctly for strings", () => { expect(Equal.equals("hello", "hello")).toBe(true) expect(Equal.equals("hello", "world")).toBe(false) }) it("should work correctly for booleans", () => { expect(Equal.equals(true, true)).toBe(true) expect(Equal.equals(false, false)).toBe(true) expect(Equal.equals(true, false)).toBe(false) }) it("should work correctly for null and undefined", () => { expect(Equal.equals(null, null)).toBe(true) expect(Equal.equals(undefined, undefined)).toBe(true) expect(Equal.equals(null, undefined)).toBe(false) }) }) describe("Date objects", () => { it("should compare dates by ISO string", () => { const date1 = new Date("2023-01-01T00:00:00.000Z") const date2 = new Date("2023-01-01T00:00:00.000Z") expect(Equal.equals(date1, date2)).toBe(true) }) it("should return false for different dates", () => { const date1 = new Date("2023-01-01T00:00:00.000Z") const date2 = new Date("2023-01-02T00:00:00.000Z") expect(Equal.equals(date1, date2)).toBe(false) }) }) describe("Effect data structures", () => { it("should work with HashMap (implements Equal interface)", () => { const map1 = HashMap.make(["a", 1], ["b", 2]) const map2 = HashMap.make(["a", 1], ["b", 2]) expect(Equal.equals(map1, map2)).toBe(true) }) it("should work with Option (implements Equal interface)", () => { const opt1 = Option.some(42) const opt2 = Option.some(42) expect(Equal.equals(opt1, opt2)).toBe(true) }) it("HashMap should not equal plain objects", () => { const map = HashMap.make(["a", 1], ["b", 2]) const obj = { a: 1, b: 2 } expect(Equal.equals(map, obj)).toBe(false) }) }) describe("custom Equal implementations", () => { class CustomPoint implements Equal.Equal { constructor(readonly x: number, readonly y: number) {} [Equal.symbol](that: Equal.Equal): boolean { return that instanceof CustomPoint && this.x === that.x && this.y === that.y } [Hash.symbol](): number { return 0 } } it("should work with custom Equal implementation", () => { const point1 = new CustomPoint(1, 2) const point2 = new CustomPoint(1, 2) expect(Equal.equals(point1, point2)).toBe(true) }) }) describe("nested structures", () => { it("should return true for nested objects (deep structural equality)", () => { const obj1 = { a: { b: 1 }, c: [1, 2] } const obj2 = { a: { b: 1 }, c: [1, 2] } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return true for nested arrays (deep structural equality)", () => { const arr1 = [[1, 2], [3, 4]] const arr2 = [[1, 2], [3, 4]] expect(Equal.equals(arr1, arr2)).toBe(true) }) it("should return false for nested objects with different values", () => { const obj1 = { a: { b: 1 }, c: [1, 2] } const obj2 = { a: { b: 2 }, c: [1, 2] } expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should return false for nested arrays with different values", () => { const arr1 = [[1, 2], [3, 4]] const arr2 = [[1, 3], [3, 4]] expect(Equal.equals(arr1, arr2)).toBe(false) }) }) describe("special values", () => { describe("NaN", () => { it("should handle NaN correctly (structural equality - NaN equals NaN)", () => { expect(Equal.equals(NaN, NaN)).toBe(true) }) it("should return false when comparing NaN with regular numbers", () => { expect(Equal.equals(NaN, 0)).toBe(false) expect(Equal.equals(NaN, 42)).toBe(false) expect(Equal.equals(42, NaN)).toBe(false) }) it("should return false when comparing NaN with Infinity", () => { expect(Equal.equals(NaN, Infinity)).toBe(false) expect(Equal.equals(NaN, -Infinity)).toBe(false) expect(Equal.equals(Infinity, NaN)).toBe(false) expect(Equal.equals(-Infinity, NaN)).toBe(false) }) it("should handle NaN in arrays", () => { expect(Equal.equals([NaN], [NaN])).toBe(true) expect(Equal.equals([NaN, 1], [NaN, 1])).toBe(true) expect(Equal.equals([NaN, 1], [NaN, 2])).toBe(false) expect(Equal.equals([NaN], [Infinity])).toBe(false) }) it("should handle NaN in objects", () => { expect(Equal.equals({ a: NaN }, { a: NaN })).toBe(true) expect(Equal.equals({ a: NaN, b: 1 }, { a: NaN, b: 1 })).toBe(true) expect(Equal.equals({ a: NaN }, { a: Infinity })).toBe(false) expect(Equal.equals({ a: NaN }, { a: 42 })).toBe(false) }) it("should handle NaN in nested structures", () => { expect(Equal.equals({ arr: [NaN] }, { arr: [NaN] })).toBe(true) expect(Equal.equals([{ val: NaN }], [{ val: NaN }])).toBe(true) expect(Equal.equals({ nested: { deep: NaN } }, { nested: { deep: NaN } })).toBe(true) expect(Equal.equals({ arr: [NaN] }, { arr: [Infinity] })).toBe(false) }) }) describe("Infinity", () => { it("should handle Infinity correctly (structural equality)", () => { expect(Equal.equals(Infinity, Infinity)).toBe(true) }) it("should return false when comparing Infinity with regular numbers", () => { expect(Equal.equals(Infinity, 0)).toBe(false) expect(Equal.equals(Infinity, 42)).toBe(false) expect(Equal.equals(42, Infinity)).toBe(false) expect(Equal.equals(Infinity, Number.MAX_VALUE)).toBe(false) }) it("should return false when comparing Infinity with -Infinity", () => { expect(Equal.equals(Infinity, -Infinity)).toBe(false) expect(Equal.equals(-Infinity, Infinity)).toBe(false) }) it("should handle Infinity in arrays", () => { expect(Equal.equals([Infinity], [Infinity])).toBe(true) expect(Equal.equals([Infinity, 1], [Infinity, 1])).toBe(true) expect(Equal.equals([Infinity, 1], [Infinity, 2])).toBe(false) expect(Equal.equals([Infinity], [-Infinity])).toBe(false) }) it("should handle Infinity in objects", () => { expect(Equal.equals({ a: Infinity }, { a: Infinity })).toBe(true) expect(Equal.equals({ a: Infinity, b: 1 }, { a: Infinity, b: 1 })).toBe(true) expect(Equal.equals({ a: Infinity }, { a: -Infinity })).toBe(false) expect(Equal.equals({ a: Infinity }, { a: 42 })).toBe(false) }) it("should handle Infinity in nested structures", () => { expect(Equal.equals({ arr: [Infinity] }, { arr: [Infinity] })).toBe(true) expect(Equal.equals([{ val: Infinity }], [{ val: Infinity }])).toBe(true) expect(Equal.equals({ nested: { deep: Infinity } }, { nested: { deep: Infinity } })).toBe(true) expect(Equal.equals({ arr: [Infinity] }, { arr: [-Infinity] })).toBe(false) }) }) describe("-Infinity", () => { it("should handle -Infinity correctly (structural equality)", () => { expect(Equal.equals(-Infinity, -Infinity)).toBe(true) }) it("should return false when comparing -Infinity with regular numbers", () => { expect(Equal.equals(-Infinity, 0)).toBe(false) expect(Equal.equals(-Infinity, -42)).toBe(false) expect(Equal.equals(-42, -Infinity)).toBe(false) expect(Equal.equals(-Infinity, Number.MIN_VALUE)).toBe(false) }) it("should handle -Infinity in arrays", () => { expect(Equal.equals([-Infinity], [-Infinity])).toBe(true) expect(Equal.equals([-Infinity, 1], [-Infinity, 1])).toBe(true) expect(Equal.equals([-Infinity, 1], [-Infinity, 2])).toBe(false) expect(Equal.equals([-Infinity], [Infinity])).toBe(false) }) it("should handle -Infinity in objects", () => { expect(Equal.equals({ a: -Infinity }, { a: -Infinity })).toBe(true) expect(Equal.equals({ a: -Infinity, b: 1 }, { a: -Infinity, b: 1 })).toBe(true) expect(Equal.equals({ a: -Infinity }, { a: Infinity })).toBe(false) expect(Equal.equals({ a: -Infinity }, { a: -42 })).toBe(false) }) it("should handle -Infinity in nested structures", () => { expect(Equal.equals({ arr: [-Infinity] }, { arr: [-Infinity] })).toBe(true) expect(Equal.equals([{ val: -Infinity }], [{ val: -Infinity }])).toBe(true) expect(Equal.equals({ nested: { deep: -Infinity } }, { nested: { deep: -Infinity } })).toBe(true) expect(Equal.equals({ arr: [-Infinity] }, { arr: [Infinity] })).toBe(false) }) }) describe("mixed special values", () => { it("should handle arrays with multiple special values", () => { expect(Equal.equals([NaN, Infinity, -Infinity], [NaN, Infinity, -Infinity])).toBe(true) expect(Equal.equals([NaN, Infinity], [Infinity, NaN])).toBe(false) // order matters expect(Equal.equals([NaN, Infinity, 0], [NaN, Infinity, 0])).toBe(true) }) it("should handle objects with multiple special values", () => { expect(Equal.equals( { nan: NaN, inf: Infinity, negInf: -Infinity }, { nan: NaN, inf: Infinity, negInf: -Infinity } )).toBe(true) expect(Equal.equals( { nan: NaN, inf: Infinity }, { nan: NaN, inf: -Infinity } )).toBe(false) }) it("should handle complex nested structures with special values", () => { const obj1 = { data: [ { value: NaN, meta: { type: "special" } }, { value: Infinity, meta: { type: "infinite" } }, { value: -Infinity, meta: { type: "negative-infinite" } } ] } const obj2 = { data: [ { value: NaN, meta: { type: "special" } }, { value: Infinity, meta: { type: "infinite" } }, { value: -Infinity, meta: { type: "negative-infinite" } } ] } const obj3 = { data: [ { value: NaN, meta: { type: "special" } }, { value: -Infinity, meta: { type: "infinite" } }, // swapped values { value: Infinity, meta: { type: "negative-infinite" } } ] } expect(Equal.equals(obj1, obj2)).toBe(true) expect(Equal.equals(obj1, obj3)).toBe(false) }) }) describe("zero values", () => { it("should handle -0 and +0 correctly", () => { expect(Equal.equals(-0, +0)).toBe(true) expect(Equal.equals(0, -0)).toBe(true) expect(Equal.equals(+0, -0)).toBe(true) }) it("should handle zero values in arrays", () => { expect(Equal.equals([-0], [+0])).toBe(true) expect(Equal.equals([0], [-0])).toBe(true) expect(Equal.equals([-0, 1], [+0, 1])).toBe(true) }) it("should handle zero values in objects", () => { expect(Equal.equals({ a: -0 }, { a: +0 })).toBe(true) expect(Equal.equals({ a: 0 }, { a: -0 })).toBe(true) }) }) }) describe("JavaScript Map", () => { it("should return true for structurally identical maps", () => { const map1 = new Map([["a", 1], ["b", 2]]) const map2 = new Map([["a", 1], ["b", 2]]) const map3 = new Map([["b", 2], ["a", 1]]) expect(Equal.equals(map1, map2)).toBe(true) expect(Equal.equals(map1, map3)).toBe(true) }) it("should return true for same reference", () => { const map = new Map([["a", 1]]) expect(Equal.equals(map, map)).toBe(true) }) it("should return true for empty maps", () => { expect(Equal.equals(new Map(), new Map())).toBe(true) }) it("should return false for maps with different values", () => { const map1 = new Map([["a", 1], ["b", 2]]) const map2 = new Map([["a", 1], ["b", 3]]) expect(Equal.equals(map1, map2)).toBe(false) }) it("should return false for maps with different keys", () => { const map1 = new Map([["a", 1], ["b", 2]]) const map2 = new Map([["a", 1], ["c", 2]]) expect(Equal.equals(map1, map2)).toBe(false) }) it("should return false for maps with different sizes", () => { const map1 = new Map([["a", 1], ["b", 2]]) const map2 = new Map([["a", 1]]) expect(Equal.equals(map1, map2)).toBe(false) }) it("should handle maps with object keys", () => { const key1 = { id: 1 } const key2 = { id: 1 } const map1 = new Map() map1.set(key1, "value1") map1.set({ nested: { x: 1 } }, "value2") const map2 = new Map() map2.set(key2, "value1") map2.set({ nested: { x: 1 } }, "value2") expect(Equal.equals(map1, map2)).toBe(true) }) it("should handle maps with object values", () => { const map1 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]]) const map2 = new Map([["a", { x: 1 }], ["b", { y: [1, 2] }]]) expect(Equal.equals(map1, map2)).toBe(true) }) it("should handle nested maps", () => { const inner1 = new Map([["x", 1]]) const inner2 = new Map([["x", 1]]) const map1 = new Map([["nested", inner1]]) const map2 = new Map([["nested", inner2]]) expect(Equal.equals(map1, map2)).toBe(true) }) it("should handle maps with special values", () => { const map1 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]]) const map2 = new Map([[NaN, "nan"], [Infinity, "inf"], [-Infinity, "neginf"]]) expect(Equal.equals(map1, map2)).toBe(true) }) }) describe("JavaScript Set", () => { it("should return true for structurally identical sets", () => { const set1 = new Set([1, 2, 3]) const set2 = new Set([1, 2, 3]) expect(Equal.equals(set1, set2)).toBe(true) }) it("should return true for same reference", () => { const set = new Set([1, 2, 3]) expect(Equal.equals(set, set)).toBe(true) }) it("should return true for empty sets", () => { expect(Equal.equals(new Set(), new Set())).toBe(true) }) it("should return false for sets with different elements", () => { const set1 = new Set([1, 2, 3]) const set2 = new Set([1, 2, 4]) expect(Equal.equals(set1, set2)).toBe(false) }) it("should return false for sets with different sizes", () => { const set1 = new Set([1, 2, 3]) const set2 = new Set([1, 2]) expect(Equal.equals(set1, set2)).toBe(false) }) it("should return true for sets with same elements in different insertion order", () => { const set1 = new Set([1, 2, 3]) const set2 = new Set([3, 1, 2]) expect(Equal.equals(set1, set2)).toBe(true) }) it("should handle sets with object elements", () => { const set1 = new Set([{ x: 1 }, { y: 2 }]) const set2 = new Set([{ x: 1 }, { y: 2 }]) expect(Equal.equals(set1, set2)).toBe(true) }) it("should handle nested sets", () => { const inner1 = new Set([1, 2]) const inner2 = new Set([1, 2]) const set1 = new Set([inner1, "other"]) const set2 = new Set([inner2, "other"]) expect(Equal.equals(set1, set2)).toBe(true) }) it("should handle sets with special values", () => { const set1 = new Set([NaN, Infinity, -Infinity]) const set2 = new Set([NaN, Infinity, -Infinity]) expect(Equal.equals(set1, set2)).toBe(true) }) it("should handle sets with array elements", () => { const set1 = new Set([[1, 2], [3, 4]]) const set2 = new Set([[1, 2], [3, 4]]) expect(Equal.equals(set1, set2)).toBe(true) }) }) describe("Map and Set mixed", () => { it("should handle objects containing maps and sets", () => { const obj1 = { map: new Map([["a", 1], ["b", 2]]), set: new Set([1, 2, 3]), array: [new Map([["x", 1]]), new Set([4, 5])] } const obj2 = { map: new Map([["a", 1], ["b", 2]]), set: new Set([1, 2, 3]), array: [new Map([["x", 1]]), new Set([4, 5])] } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return false when Map/Set differ in nested structures", () => { const obj1 = { map: new Map([["a", 1], ["b", 2]]), set: new Set([1, 2, 3]) } const obj2 = { map: new Map([["a", 1], ["b", 3]]), // different value set: new Set([1, 2, 3]) } expect(Equal.equals(obj1, obj2)).toBe(false) }) }) describe("byReference", () => { it("should allow objects to opt out of structural equality", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Normal structural equality expect(Equal.equals(obj1, obj2)).toBe(true) // Create instance equality version of obj1 const obj1Instance = Equal.byReference(obj1) expect(Equal.equals(obj1Instance, obj2)).toBe(false) expect(Equal.equals(obj1Instance, obj1Instance)).toBe(true) }) it("should work with arrays", () => { const arr1 = [1, 2, 3] const arr2 = [1, 2, 3] // Normal structural equality expect(Equal.equals(arr1, arr2)).toBe(true) // Create instance equality version of arr1 const arr1Instance = Equal.byReference(arr1) expect(Equal.equals(arr1Instance, arr2)).toBe(false) expect(Equal.equals(arr1Instance, arr1Instance)).toBe(true) }) it("should work with Maps", () => { const map1 = new Map([["a", 1]]) const map2 = new Map([["a", 1]]) // Normal structural equality expect(Equal.equals(map1, map2)).toBe(true) // Create instance equality version of map1 const map1Instance = Equal.byReference(map1) expect(Equal.equals(map1Instance, map2)).toBe(false) expect(Equal.equals(map1Instance, map1Instance)).toBe(true) }) it("should work with Sets", () => { const set1 = new Set([1, 2]) const set2 = new Set([1, 2]) // Normal structural equality expect(Equal.equals(set1, set2)).toBe(true) // Create instance equality version of set1 const set1Instance = Equal.byReference(set1) expect(Equal.equals(set1Instance, set2)).toBe(false) expect(Equal.equals(set1Instance, set1Instance)).toBe(true) }) it("should return a proxy that behaves like the original object", () => { const obj = { a: 1 } const result = Equal.byReference(obj) // The result should not be the same reference (it's a proxy) expect(result).not.toBe(obj) // But it should behave the same way expect(result.a).toBe(1) // And should support property access expect(result.a).toBe(obj.a) }) it("should work when either object is marked for instance equality", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Create instance equality version of obj2 const obj2Instance = Equal.byReference(obj2) expect(Equal.equals(obj1, obj2Instance)).toBe(false) expect(Equal.equals(obj2Instance, obj1)).toBe(false) }) it("should work with nested structures", () => { const obj1 = { a: { b: 1 }, c: [1, 2] } const obj2 = { a: { b: 1 }, c: [1, 2] } // Normal structural equality expect(Equal.equals(obj1, obj2)).toBe(true) // Create instance equality version of obj1 const obj1Instance = Equal.byReference(obj1) expect(Equal.equals(obj1Instance, obj2)).toBe(false) }) }) describe("byReferenceUnsafe", () => { it("should allow objects to opt out of structural equality without proxy", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Mark obj1 for reference equality (modifies obj1 directly) const obj1ByRef = Equal.byReferenceUnsafe(obj1) expect(obj1ByRef).toBe(obj1) // Same object, no proxy created expect(Equal.equals(obj1ByRef, obj2)).toBe(false) // uses reference equality expect(Equal.equals(obj1ByRef, obj1ByRef)).toBe(true) // same reference // The original obj1 now uses reference equality expect(Equal.equals(obj1, obj2)).toBe(false) // obj1 uses reference equality }) it("should work with arrays", () => { const arr1 = [1, 2, 3] const arr2 = [1, 2, 3] // Mark arr1 for reference equality const arr1ByRef = Equal.byReferenceUnsafe(arr1) expect(arr1ByRef).toBe(arr1) // Same array, no proxy expect(Equal.equals(arr1ByRef, arr2)).toBe(false) expect(Equal.equals(arr1ByRef, arr1ByRef)).toBe(true) }) it("should work with Maps", () => { const map1 = new Map([["a", 1]]) const map2 = new Map([["a", 1]]) // Mark map1 for reference equality const map1ByRef = Equal.byReferenceUnsafe(map1) expect(map1ByRef).toBe(map1) // Same map, no proxy expect(Equal.equals(map1ByRef, map2)).toBe(false) expect(Equal.equals(map1ByRef, map1ByRef)).toBe(true) }) it("should work with Sets", () => { const set1 = new Set([1, 2]) const set2 = new Set([1, 2]) // Mark set1 for reference equality const set1ByRef = Equal.byReferenceUnsafe(set1) expect(set1ByRef).toBe(set1) // Same set, no proxy expect(Equal.equals(set1ByRef, set2)).toBe(false) expect(Equal.equals(set1ByRef, set1ByRef)).toBe(true) }) it("should permanently change object equality behavior", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Mark obj1 for reference equality Equal.byReferenceUnsafe(obj1) // Now obj1 permanently uses reference equality expect(Equal.equals(obj1, obj2)).toBe(false) expect(Equal.equals(obj1, obj1)).toBe(true) }) it("should work when either object is marked for reference equality", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Mark obj2 for reference equality Equal.byReferenceUnsafe(obj2) expect(Equal.equals(obj1, obj2)).toBe(false) expect(Equal.equals(obj2, obj1)).toBe(false) }) it("should work with nested structures", () => { const obj1 = { a: { b: 1 }, c: [1, 2] } const obj2 = { a: { b: 1 }, c: [1, 2] } // Mark obj1 for reference equality Equal.byReferenceUnsafe(obj1) expect(Equal.equals(obj1, obj2)).toBe(false) }) }) describe("Error objects", () => { it("should return true for Error objects with same message and additional properties", () => { const error1 = new Error("test message") ;(error1 as any).code = "TEST_CODE" ;(error1 as any).details = { info: "additional details" } const error2 = new Error("test message") ;(error2 as any).code = "TEST_CODE" ;(error2 as any).details = { info: "additional details" } // Error objects are now structurally comparable (stack is excluded) expect(Equal.equals(error1, error2)).toBe(true) }) it("should return false for Error objects with different messages", () => { const error1 = new Error("message 1") ;(error1 as any).code = "TEST_CODE" const error2 = new Error("message 2") ;(error2 as any).code = "TEST_CODE" expect(Equal.equals(error1, error2)).toBe(false) }) it("should work with custom Error classes", () => { class CustomError extends Error { readonly code: string constructor(message: string, code: string) { super(message) this.name = "CustomError" this.code = code } } const error1 = new CustomError("test", "CUSTOM") const error2 = new CustomError("test", "CUSTOM") // Custom Error objects are now structurally comparable (stack is excluded) expect(Equal.equals(error1, error2)).toBe(true) }) it("should ignore stack traces when comparing Error objects", () => { function createErrorAtDifferentLocation1() { return new Error("same message") } function createErrorAtDifferentLocation2() { return new Error("same message") } const error1 = createErrorAtDifferentLocation1() const error2 = createErrorAtDifferentLocation2() // Even though stack traces are different, errors should be equal expect(error1.stack).not.toBe(error2.stack) expect(Equal.equals(error1, error2)).toBe(true) }) }) describe("objects with different constructors", () => { it("should return true for objects with different constructors but same shape", () => { class CustomClass { readonly a: number readonly b: number constructor() { this.a = 1 this.b = 2 } } const plainObject = { a: 1, b: 2 } const customObject = new CustomClass() expect(Equal.equals(plainObject, customObject)).toBe(true) }) it("should return true for objects with null prototype", () => { const nullProtoObject: any = Object.create(null) nullProtoObject.a = 1 nullProtoObject.b = 2 const plainObject = { a: 1, b: 2 } expect(Equal.equals(nullProtoObject, plainObject)).toBe(true) }) it("should return true for objects with different function constructors", () => { function Constructor1(this: any) { this.a = 1 this.b = 2 } function Constructor2(this: any) { this.a = 1 this.b = 2 } const obj1 = new (Constructor1 as any)() const obj2 = new (Constructor2 as any)() expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should return false when properties differ regardless of constructor", () => { class CustomClass1 { readonly a: number readonly b: number constructor() { this.a = 1 this.b = 2 } } class CustomClass2 { readonly a: number readonly b: number constructor() { this.a = 1 this.b = 3 // Different value } } const obj1 = new CustomClass1() const obj2 = new CustomClass2() expect(Equal.equals(obj1, obj2)).toBe(false) }) it("should handle mixed constructor types in nested structures", () => { class CustomClass { readonly inner: { x: number } constructor() { this.inner = { x: 1 } } } const obj1 = { data: new CustomClass() } const obj2 = { data: { inner: { x: 1 } } } expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should include constructor property when it has a meaningful user-defined value", () => { const obj1 = { constructor: 10, a: 1 } const obj2 = { constructor: 10, a: 1 } const obj3 = { constructor: 20, a: 1 } // Objects with same constructor value should be equal expect(Equal.equals(obj1, obj2)).toBe(true) // Objects with different constructor values should not be equal expect(Equal.equals(obj1, obj3)).toBe(false) expect(Equal.equals(obj2, obj3)).toBe(false) }) it("should include constructor property when it has a meaningful user-defined value from a prototype", () => { const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 }) const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: 10 }) const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: 20 }) // Objects with same constructor value should be equal expect(Equal.equals(obj1, obj2)).toBe(true) // Objects with different constructor values should not be equal expect(Equal.equals(obj1, obj3)).toBe(false) expect(Equal.equals(obj2, obj3)).toBe(false) }) it("should include constructor property when it has a meaningful user-defined value from a prototype when it is a function", () => { const f = () => 10 const g = () => 10 const obj1 = Object.setPrototypeOf({ a: 1 }, { constructor: f }) const obj2 = Object.setPrototypeOf({ a: 1 }, { constructor: f }) const obj3 = Object.setPrototypeOf({ a: 1 }, { constructor: g }) // Objects with same constructor value should be equal expect(Equal.equals(obj1, obj2)).toBe(true) // Objects with different constructor values should not be equal expect(Equal.equals(obj1, obj3)).toBe(false) expect(Equal.equals(obj2, obj3)).toBe(false) }) it("should still ignore default constructor property", () => { const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } // Normal objects should still be equal (constructor property ignored when it's the default) expect(Equal.equals(obj1, obj2)).toBe(true) // Verify that obj1.constructor and obj2.constructor are the default Object constructor expect(obj1.constructor).toBe(Object) expect(obj2.constructor).toBe(Object) }) }) describe("recursive objects", () => { it("should handle circular references in Equal implementations without infinite recursion", () => { class CircularEqualTest implements Equal.Equal { constructor(readonly value: string, public child: CircularEqualTest | null = null) {} [Equal.symbol](that: Equal.Equal): boolean { if (!(that instanceof CircularEqualTest)) { return false } // Circular references are handled automatically by the system // This implementation can safely call Equal.equals without manual tracking return this.value === that.value && Equal.equals(this.child, that.child) } [Hash.symbol](): number { return Hash.string(this.value) } } // Test case 1: Self-referencing objects const obj1 = new CircularEqualTest("test") obj1.child = obj1 const obj2 = new CircularEqualTest("test") obj2.child = obj2 // This should not cause infinite recursion expect(Equal.equals(obj1, obj2)).toBe(true) // Test case 2: Different values should not be equal even with same structure const obj3 = new CircularEqualTest("different") obj3.child = obj3 expect(Equal.equals(obj1, obj3)).toBe(false) }) it("should handle complex circular references in Equal implementations", () => { class ComplexEqualTest implements Equal.Equal { constructor(readonly id: number, public ref: ComplexEqualTest | null = null) {} [Equal.symbol](that: Equal.Equal): boolean { if (!(that instanceof ComplexEqualTest)) { return false } // Circular references are handled automatically by the system return this.id === that.id && Equal.equals(this.ref, that.ref) } [Hash.symbol](): number { return Hash.number(this.id) } } // Create cycle: obj1 -> obj2 -> obj1 const obj1 = new ComplexEqualTest(1) const obj2 = new ComplexEqualTest(2) obj1.ref = obj2 obj2.ref = obj1 // Create identical cycle: obj3 -> obj4 -> obj3 const obj3 = new ComplexEqualTest(1) const obj4 = new ComplexEqualTest(2) obj3.ref = obj4 obj4.ref = obj3 // Should not cause infinite recursion and should be equal expect(Equal.equals(obj1, obj3)).toBe(true) expect(Equal.equals(obj2, obj4)).toBe(true) }) it("should expose visited sets for advanced use cases", () => { // Most implementations don't need manual tracking, but the sets are exposed // for advanced scenarios where integration with the tracking system is needed class SimpleContainer implements Equal.Equal { constructor(readonly name: string, readonly items: Array = []) {} [Equal.symbol](that: Equal.Equal): boolean { if (!(that instanceof SimpleContainer)) { return false } // Simple implementation - no manual tracking needed // Circular references are handled automatically return this.name === that.name && this.items.length === that.items.length && this.items.every((item, i) => Equal.equals(item, that.items[i])) } [Hash.symbol](): number { // Simple hash implementation - circular references are handled automatically let result = Hash.string(this.name) for (const item of this.items) { result = Hash.combine(result, Hash.hash(item)) } return result } } // Test that automatic circular reference handling works const container1 = new SimpleContainer("A") container1.items.push(container1) // Self-reference const container2 = new SimpleContainer("A") container2.items.push(container2) // Self-reference // Should not cause infinite recursion due to automatic tracking expect(Equal.equals(container1, container2)).toBe(true) }) it("should handle objects with circular references without infinite recursion", () => { // Create objects that reference themselves const obj1: any = { a: 1, b: 2 } obj1.self = obj1 const obj2: any = { a: 1, b: 2 } obj2.self = obj2 // This should not throw due to stack overflow expect(() => Equal.equals(obj1, obj2)).not.toThrow() expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should handle arrays with circular references without infinite recursion", () => { // Create arrays that reference themselves const arr1: any = [1, 2, 3] arr1.push(arr1) const arr2: any = [1, 2, 3] arr2.push(arr2) // This should not throw due to stack overflow expect(() => Equal.equals(arr1, arr2)).not.toThrow() expect(Equal.equals(arr1, arr2)).toBe(true) }) it("should handle Uint8Array", () => { const arr1: Uint8Array = new Uint8Array([1, 2, 3]) const arr2: Uint8Array = new Uint8Array([1, 2, 3]) expect(Equal.equals(arr1, arr2)).toBe(true) }) it("should handle mixed circular references between objects and arrays", () => { // Create complex circular structure const obj1: any = { type: "object", items: [] } const arr1: any = [1, 2, obj1] obj1.items = arr1 const obj2: any = { type: "object", items: [] } const arr2: any = [1, 2, obj2] obj2.items = arr2 // This should not throw due to stack overflow expect(() => Equal.equals(obj1, obj2)).not.toThrow() expect(Equal.equals(obj1, obj2)).toBe(true) }) it("should handle hashing objects with circular references without infinite recursion", () => { // Create object that references itself const obj: any = { a: 1, b: 2 } obj.self = obj // This should not throw due to stack overflow expect(() => Hash.hash(obj)).not.toThrow() // Hash should be consistent const hash1 = Hash.hash(obj) const hash2 = Hash.hash(obj) expect(hash1).toBe(hash2) }) it("should handle hashing arrays with circular references without infinite recursion", () => { // Create array that references itself const arr: any = [1, 2, 3] arr.push(arr) // This should not throw due to stack overflow expect(() => Hash.hash(arr)).not.toThrow() // Hash should be consistent const hash1 = Hash.hash(arr) const hash2 = Hash.hash(arr) expect(hash1).toBe(hash2) }) it("should produce different hashes for objects with different circular structures", () => { const obj1: any = { a: 1, b: 2 } obj1.self = obj1 const obj2: any = { a: 1, b: 3 } // Different value obj2.self = obj2 // Should not throw and should produce different hashes expect(() => Hash.hash(obj1)).not.toThrow() expect(() => Hash.hash(obj2)).not.toThrow() expect(Hash.hash(obj1)).not.toBe(Hash.hash(obj2)) }) it("should return false for objects with different circular reference patterns", () => { // Object that references itself const obj1: any = { a: 1, b: 2 } obj1.self = obj1 // Object that references a different object const obj2: any = { a: 1, b: 2 } const obj3: any = { a: 1, b: 2 } obj2.self = obj3 // These should be considered different expect(() => Equal.equals(obj1, obj2)).not.toThrow() expect(Equal.equals(obj1, obj2)).toBe(false) }) }) })