From a5375250994de093d21771a66de471f54dce037c Mon Sep 17 00:00:00 2001
From: Kir_Antipov <kp.antipov@gmail.com>
Date: Tue, 6 Dec 2022 14:22:13 +0000
Subject: [PATCH] Implemented equality comparers

---
 .../comparison/composite-equality-comparer.ts | 137 ++++++++++++++++++
 src/utils/comparison/equality-comparer.ts     |  52 +++++++
 .../comparison/equality-comparer.utils.ts     |  42 ++++++
 .../composite-equality-comparer.spec.ts       |  96 ++++++++++++
 .../comparison/equality-comparer.spec.ts      |  95 ++++++++++++
 5 files changed, 422 insertions(+)
 create mode 100644 src/utils/comparison/composite-equality-comparer.ts
 create mode 100644 src/utils/comparison/equality-comparer.ts
 create mode 100644 src/utils/comparison/equality-comparer.utils.ts
 create mode 100644 tests/unit/utils/comparison/composite-equality-comparer.spec.ts
 create mode 100644 tests/unit/utils/comparison/equality-comparer.spec.ts

diff --git a/src/utils/comparison/composite-equality-comparer.ts b/src/utils/comparison/composite-equality-comparer.ts
new file mode 100644
index 0000000..72087ae
--- /dev/null
+++ b/src/utils/comparison/composite-equality-comparer.ts
@@ -0,0 +1,137 @@
+import { CALL, makeCallable } from "@/utils/functions/callable";
+import { andEqualityComparers, negateEqualityComparer, orEqualityComparers } from "./equality-comparer.utils";
+import { EqualityComparer } from "./equality-comparer";
+
+/**
+ * A class that represents a composite equality comparer.
+ *
+ * @template T - The type of the elements to compare.
+ */
+export interface CompositeEqualityComparer<T> extends EqualityComparer<T> {
+    /**
+     * Compares two values for equality.
+     *
+     * @param x - The first value to compare.
+     * @param y - The second value to compare.
+     *
+     * @returns `true` if the values are equal; otherwise, `false`.
+     */
+    (x: T, y: T): boolean;
+}
+
+/**
+ * A class that represents a composite equality comparer.
+ *
+ * @template T - The type of the elements to compare.
+ */
+export class CompositeEqualityComparer<T> {
+    /**
+     * The underlying comparer function used for comparison.
+     */
+    private readonly _comparer: EqualityComparer<T>;
+
+    /**
+     * The negated version of this comparer.
+     */
+    private _negated?: CompositeEqualityComparer<T>;
+
+    /**
+     * Creates a new instance of {@link CompositeEqualityComparer}.
+     *
+     * @param comparer - An underlying comparer that should be used for comparison.
+     * @param inverted - A cached inverted {@link CompositeEqualityComparer} instance, if any.
+     *
+     * @remarks
+     *
+     * Should **not** be called directly. Use {@link create}, or {@link createInternal} instead.
+     */
+    private constructor(comparer: EqualityComparer<T>, inverted: CompositeEqualityComparer<T>) {
+        this._comparer = comparer;
+        this._negated = inverted;
+    }
+
+    /**
+     * Creates a new instance of {@link CompositeEqualityComparer}.
+     *
+     * @template T - The type of the elements to compare.
+     * @param comparer - An underlying comparer that should be used for comparison.
+     * @param inverted - A cached inverted {@link CompositeEqualityComparer} instance, if any.
+     *
+     * @returns A new instance of {@link CompositeEqualityComparer}.
+     */
+    private static createInternal<T>(comparer: EqualityComparer<T>, inverted?: CompositeEqualityComparer<T>): CompositeEqualityComparer<T> {
+        return makeCallable(new CompositeEqualityComparer(comparer, inverted));
+    }
+
+    /**
+     * Creates a new instance of {@link CompositeEqualityComparer}.
+     *
+     * @template T - The type of the elements to compare.
+     * @param comparer - An underlying comparer that should be used for comparison.
+     *
+     * @returns A new instance of {@link CompositeEqualityComparer}.
+     */
+    static create<T>(comparer: EqualityComparer<T>): CompositeEqualityComparer<T> {
+        return CompositeEqualityComparer.createInternal(comparer);
+    }
+
+    /**
+     * Compares two values for equality.
+     *
+     * @param x - The first value to compare.
+     * @param y - The second value to compare.
+     *
+     * @returns `true` if the values are equal; otherwise, `false`.
+     */
+    equals(x: T, y: T): boolean {
+        return this._comparer(x, y);
+    }
+
+    /**
+     * Compares two values for equality.
+     *
+     * @param x - The first value to compare.
+     * @param y - The second value to compare.
+     *
+     * @returns `true` if the values are equal; otherwise, `false`.
+     */
+    [CALL](x: T, y: T): boolean {
+        return this._comparer(x, y);
+    }
+
+    /**
+     * Combines this comparer with another using the logical OR operator.
+     *
+     * @param comparer - The other comparer to combine with.
+     *
+     * @returns A new composite equality comparer representing the combination of this and the other comparer.
+     */
+    or(comparer: EqualityComparer<T>): CompositeEqualityComparer<T> {
+        const unwrappedComparer = comparer instanceof CompositeEqualityComparer ? comparer._comparer : comparer;
+        const combinedComparer = orEqualityComparers(this._comparer, unwrappedComparer);
+        return CompositeEqualityComparer.createInternal(combinedComparer);
+    }
+
+    /**
+     * Combines this comparer with another using the logical AND operator.
+     *
+     * @param comparer - The other comparer to combine with.
+     *
+     * @returns A new composite equality comparer representing the combination of this and the other comparer.
+     */
+    and(comparer: EqualityComparer<T>): CompositeEqualityComparer<T> {
+        const unwrappedComparer = comparer instanceof CompositeEqualityComparer ? comparer._comparer : comparer;
+        const combinedComparer = andEqualityComparers(this._comparer, unwrappedComparer);
+        return CompositeEqualityComparer.createInternal(combinedComparer);
+    }
+
+    /**
+     * Negates this comparer using the logical NOT operator.
+     *
+     * @returns A new composite equality comparer representing the negation of this comparer.
+     */
+    negate(): CompositeEqualityComparer<T> {
+        this._negated ??= CompositeEqualityComparer.createInternal(negateEqualityComparer(this._comparer), this);
+        return this._negated;
+    }
+}
diff --git a/src/utils/comparison/equality-comparer.ts b/src/utils/comparison/equality-comparer.ts
new file mode 100644
index 0000000..aeea0bd
--- /dev/null
+++ b/src/utils/comparison/equality-comparer.ts
@@ -0,0 +1,52 @@
+import { CompositeEqualityComparer } from "./composite-equality-comparer";
+
+/**
+ * A function that compares two values for equality.
+ *
+ * @template T - The type of values being compared.
+ */
+export interface EqualityComparer<T> {
+    /**
+     * Compares two values for equality.
+     *
+     * @param x - The first value to compare.
+     * @param y - The second value to compare.
+     *
+     * @returns `true` if the values are equal; otherwise, `false`.
+     */
+    (x: T, y: T): boolean;
+}
+
+/**
+ * Creates a composite equality comparer from the specified function.
+ *
+ * @template T - The type of values being compared.
+ *
+ * @param comparer - The equality comparer function to use as the base comparer.
+ *
+ * @returns A new {@link CompositeEqualityComparer} object.
+ */
+export function createEqualityComparer<T>(comparer: EqualityComparer<T>): CompositeEqualityComparer<T> {
+    return CompositeEqualityComparer.create(comparer);
+}
+
+// These functions were moved to a different file because of problems with circular references.
+export {
+    negateEqualityComparer,
+    orEqualityComparers,
+    andEqualityComparers,
+} from "./equality-comparer.utils";
+
+/**
+ * The default equality comparer that uses strict equality (`===`) to compare values.
+ */
+const DEFAULT_EQUALITY_COMPARER = createEqualityComparer<unknown>((x, y) => x === y);
+
+/**
+ * Creates a composite equality comparer that uses strict equality (`===`) to compare values.
+ *
+ * @template T - The type of values being compared.
+ */
+export function createDefaultEqualityComparer<T>(): CompositeEqualityComparer<T> {
+    return DEFAULT_EQUALITY_COMPARER as CompositeEqualityComparer<T>;
+}
diff --git a/src/utils/comparison/equality-comparer.utils.ts b/src/utils/comparison/equality-comparer.utils.ts
new file mode 100644
index 0000000..d7868c5
--- /dev/null
+++ b/src/utils/comparison/equality-comparer.utils.ts
@@ -0,0 +1,42 @@
+import { EqualityComparer } from "./equality-comparer";
+
+/**
+ * Returns a new equality comparer that is the negation of the specified comparer.
+ *
+ * @template T - The type of values being compared.
+ *
+ * @param comparer - The equality comparer to negate.
+ *
+ * @returns A new equality comparer that returns `true` when the specified comparer returns `false`, and vice versa.
+ */
+export function negateEqualityComparer<T>(comparer: EqualityComparer<T>): EqualityComparer<T> {
+    return (x, y) => !comparer(x, y);
+}
+
+/**
+ * Combines two equality comparers using the logical OR operator.
+ *
+ * @template T - The type of values being compared.
+ *
+ * @param left - The first equality comparer to use in the OR operation.
+ * @param right - The second equality comparer to use in the OR operation.
+ *
+ * @returns A new equality comparer that returns `true` if either the `left` or `right` comparer returns `true`.
+ */
+export function orEqualityComparers<T>(left: EqualityComparer<T>, right: EqualityComparer<T>): EqualityComparer<T> {
+    return (x, y) => left(x, y) || right(x, y);
+}
+
+/**
+ * Combines two equality comparers using the logical AND operator.
+ *
+ * @template T - The type of values being compared.
+ *
+ * @param left - The first equality comparer to use in the AND operation.
+ * @param right - The second equality comparer to use in the AND operation.
+ *
+ * @returns A new equality comparer that returns `true` if both the `left` and `right` comparers return `true`.
+ */
+export function andEqualityComparers<T>(left: EqualityComparer<T>, right: EqualityComparer<T>): EqualityComparer<T> {
+    return (x, y) => left(x, y) && right(x, y);
+}
diff --git a/tests/unit/utils/comparison/composite-equality-comparer.spec.ts b/tests/unit/utils/comparison/composite-equality-comparer.spec.ts
new file mode 100644
index 0000000..c284c1a
--- /dev/null
+++ b/tests/unit/utils/comparison/composite-equality-comparer.spec.ts
@@ -0,0 +1,96 @@
+import { CompositeEqualityComparer } from "@/utils/comparison/composite-equality-comparer";
+
+describe("CompositeEqualityComparer", () => {
+    describe("create", () => {
+        test("creates a new instance from the given equality comparer", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b);
+
+            expect(comparer).toBeInstanceOf(CompositeEqualityComparer);
+        });
+    });
+
+    describe("equals", () => {
+        test("returns true when values are equal", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b);
+
+            expect(comparer.equals(0, 0)).toBe(true);
+            expect(comparer.equals(1, 1)).toBe(true);
+            expect(comparer.equals(2, 2)).toBe(true);
+        });
+
+        test("returns false when values are not equal", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b);
+
+            expect(comparer.equals(2, 0)).toBe(false);
+            expect(comparer.equals(0, 1)).toBe(false);
+            expect(comparer.equals(1, 2)).toBe(false);
+        });
+    });
+
+    describe("__invoke__", () => {
+        test("can be used as a function", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b);
+
+            expect(comparer(0, 0)).toBe(true);
+            expect(comparer(1, 1)).toBe(true);
+            expect(comparer(2, 2)).toBe(true);
+            expect(comparer(2, 0)).toBe(false);
+            expect(comparer(0, 1)).toBe(false);
+            expect(comparer(1, 2)).toBe(false);
+        });
+    });
+
+    describe("or", () => {
+        test("returns true when either comparer returns true", () => {
+            const comparerA = (x: number, y: number) => x === y;
+            const comparerB = (x: number, y: number) => x < y;
+
+            const comparer = CompositeEqualityComparer.create(comparerA).or(comparerB);
+
+            expect(comparer.equals(1, 2)).toBe(true);
+        });
+
+        test("returns false when both comparers return false", () => {
+            const comparerA = (x: number, y: number) => x === y;
+            const comparerB = (x: number, y: number) => x < y;
+
+            const comparer = CompositeEqualityComparer.create(comparerA).or(comparerB);
+
+            expect(comparer.equals(2, 1)).toBe(false);
+        });
+    });
+
+    describe("and", () => {
+        test("returns true when both comparers return true", () => {
+            const comparerA = (x: number, y: number) => x === y;
+            const comparerB = (x: number, y: number) => x % 2 === y;
+
+            const comparer = CompositeEqualityComparer.create(comparerA).and(comparerB);
+
+            expect(comparer.equals(1, 1)).toBe(true);
+        });
+
+        test("returns false when either comparer returns false", () => {
+            const comparerA = (x: number, y: number) => x === y;
+            const comparerB = (x: number, y: number) => x % 2 === y;
+
+            const comparer = CompositeEqualityComparer.create(comparerA).and(comparerB);
+
+            expect(comparer.equals(2, 2)).toBe(false);
+        });
+    });
+
+    describe("negate", () => {
+        test("returns true when original comparer returns false", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b).negate();
+
+            expect(comparer.equals(1, 2)).toBe(true);
+        });
+
+        test("returns false when original comparer returns true", () => {
+            const comparer = CompositeEqualityComparer.create((a: number, b: number) => a === b).negate();
+
+            expect(comparer.equals(1, 1)).toBe(false);
+        });
+    });
+});
diff --git a/tests/unit/utils/comparison/equality-comparer.spec.ts b/tests/unit/utils/comparison/equality-comparer.spec.ts
new file mode 100644
index 0000000..f8bde7d
--- /dev/null
+++ b/tests/unit/utils/comparison/equality-comparer.spec.ts
@@ -0,0 +1,95 @@
+import { CompositeEqualityComparer } from "@/utils/comparison/composite-equality-comparer";
+import {
+    andEqualityComparers,
+    createDefaultEqualityComparer,
+    createEqualityComparer,
+    negateEqualityComparer,
+    orEqualityComparers,
+} from "@/utils/comparison/equality-comparer";
+
+describe("createEqualityComparer", () => {
+    test("creates a new CompositeEqualityComparer instance from the given equality comparer", () => {
+        const comparer = createEqualityComparer((a: number, b: number) => a === b);
+
+        expect(comparer).toBeInstanceOf(CompositeEqualityComparer);
+    });
+});
+
+describe("orEqualityComparers", () => {
+    test("returns true when either comparer returns true", () => {
+        const comparerA = (x: number, y: number) => x === y;
+        const comparerB = (x: number, y: number) => x < y;
+
+        const comparer = orEqualityComparers(comparerA, comparerB);
+
+        expect(comparer(1, 2)).toBe(true);
+    });
+
+    test("returns false when both comparers return false", () => {
+        const comparerA = (x: number, y: number) => x === y;
+        const comparerB = (x: number, y: number) => x < y;
+
+        const comparer = orEqualityComparers(comparerA, comparerB);
+
+        expect(comparer(2, 1)).toBe(false);
+    });
+});
+
+describe("andEqualityComparers", () => {
+    test("returns true when both comparers return true", () => {
+        const comparerA = (x: number, y: number) => x === y;
+        const comparerB = (x: number, y: number) => x % 2 === y;
+
+        const comparer = andEqualityComparers(comparerA, comparerB);
+
+        expect(comparer(1, 1)).toBe(true);
+    });
+
+    test("returns false when either comparer returns false", () => {
+        const comparerA = (x: number, y: number) => x === y;
+        const comparerB = (x: number, y: number) => x % 2 === y;
+
+        const comparer = andEqualityComparers(comparerA, comparerB);
+
+        expect(comparer(2, 2)).toBe(false);
+    });
+});
+
+describe("negateEqualityComparer", () => {
+    test("returns true when original comparer returns false", () => {
+        const comparer = negateEqualityComparer((a: number, b: number) => a === b);
+
+        expect(comparer(1, 2)).toBe(true);
+    });
+
+    test("returns false when original comparer returns true", () => {
+        const comparer = negateEqualityComparer((a: number, b: number) => a === b);
+
+        expect(comparer(1, 1)).toBe(false);
+    });
+});
+
+describe("createDefaultEqualityComparer", () => {
+    test("returns true for strictly equal values", () => {
+        const comparer = createDefaultEqualityComparer();
+
+        const sameRef = {};
+        expect(comparer(1, 1)).toBe(true);
+        expect(comparer("test", "test")).toBe(true);
+        expect(comparer(sameRef, sameRef)).toBe(true);
+        expect(comparer(Symbol.toStringTag, Symbol.toStringTag)).toBe(true);
+        expect(comparer(null, null)).toBe(true);
+        expect(comparer(undefined, undefined)).toBe(true);
+    });
+
+    test("returns false for not strictly equal values", () => {
+        const comparer = createDefaultEqualityComparer();
+
+        expect(comparer(1, 2)).toBe(false);
+        expect(comparer(1, "1")).toBe(false);
+        expect(comparer("test", "tset")).toBe(false);
+        expect(comparer({}, {})).toBe(false);
+        expect(comparer(Symbol("Symbol"), Symbol("Symbol"))).toBe(false);
+        expect(comparer(null, undefined)).toBe(false);
+    });
+});