mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-25 09:51:01 -05:00
Implemented equality comparers
This commit is contained in:
parent
ce5227a2ce
commit
a537525099
5 changed files with 422 additions and 0 deletions
137
src/utils/comparison/composite-equality-comparer.ts
Normal file
137
src/utils/comparison/composite-equality-comparer.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
52
src/utils/comparison/equality-comparer.ts
Normal file
52
src/utils/comparison/equality-comparer.ts
Normal file
|
@ -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>;
|
||||
}
|
42
src/utils/comparison/equality-comparer.utils.ts
Normal file
42
src/utils/comparison/equality-comparer.utils.ts
Normal file
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
95
tests/unit/utils/comparison/equality-comparer.spec.ts
Normal file
95
tests/unit/utils/comparison/equality-comparer.spec.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue