Implemented comparers

This commit is contained in:
Kir_Antipov 2022-12-07 19:56:28 +00:00
parent a537525099
commit d1b1ed9a16
5 changed files with 449 additions and 0 deletions

View file

@ -0,0 +1,93 @@
import { CompositeComparer } from "./composite-comparer";
/**
* A comparator function that returns a number that represents the comparison result between two elements.
*
* - If the returned number is **negative**, it means `left` is **less** than `right`.
* - If the returned number is **zero**, it means `left` is **equal** to `right`.
* - If the returned number is **positive**, it means `left` is **greater** than `right`.
*
* @template T - The type of the elements to compare.
*/
export interface Comparer<T> {
/**
* Compares two elements and returns a value indicating whether one element is less than, equal to, or greater than the other.
*
* @param left - The first element to compare.
* @param right - The second element to compare.
*
* @returns A number that represents the comparison result.
*/
(left: T, right: T): number;
}
/**
* Creates a new {@link CompositeComparer} instance based on the specified `comparer`.
*
* @template T - The type of the elements being compared.
* @param comparer - The base {@link Comparer} used to create the new {@link CompositeComparer}.
*
* @returns A new {@link CompositeComparer} instance.
*/
export function createComparer<T>(comparer: Comparer<T>): CompositeComparer<T> {
return CompositeComparer.create(comparer);
}
// These functions were moved to a different file because of problems with circular references.
export {
convertComparerToEqualityComparer,
invertComparer,
combineComparers,
} from "./comparer.utils";
/**
* The base comparer that can compare any two values.
*
* It treats `undefined` as smaller than any other value, and `null` as smaller than any value except `undefined`.
* Any non-null and non-undefined values are considered equal.
*/
const BASE_COMPARER: CompositeComparer<unknown> = createComparer<unknown>((left, right) => {
if (left === undefined) {
return right === undefined ? 0 : -1;
}
if (left === null) {
return right === undefined ? 1 : right === null ? 0 : -1;
}
if (right === undefined || right === null) {
return 1;
}
return 0;
});
/**
* The default comparer that compares two values using their natural order
* defined by the built-in `>` and `<` operators.
*/
const DEFAULT_COMPARER: CompositeComparer<unknown> = BASE_COMPARER.thenBy(
(left, right) => left < right ? -1 : left > right ? 1 : 0
);
/**
* Creates a base comparer that can compare any two values.
*
* It treats `undefined` as smaller than any other value, and `null` as smaller than any value except `undefined`.
* Any non-null and non-undefined values are considered equal.
*
* @template T - The type of the elements being compared.
*/
export function createBaseComparer<T>(): CompositeComparer<T> {
return BASE_COMPARER as CompositeComparer<T>;
}
/**
* Creates a default comparer that compares two values using their natural order
* defined by the built-in `>` and `<` operators.
*
* @template T - The type of the elements being compared.
*/
export function createDefaultComparer<T>(): CompositeComparer<T> {
return DEFAULT_COMPARER as CompositeComparer<T>;
}

View file

@ -0,0 +1,43 @@
import { Comparer } from "./comparer";
import { EqualityComparer } from "./equality-comparer";
/**
* Converts a comparer function into an equality comparer function.
* The resulting equality comparer function returns `true` if the comparer returns `0`.
*
* @param comparer - The comparer function to convert.
*
* @returns An equality comparer function that returns `true` if the comparer returns `0`.
*/
export function convertComparerToEqualityComparer<T>(comparer: Comparer<T>): EqualityComparer<T> {
return (x, y) => comparer(x, y) === 0;
}
/**
* Returns a new comparer function that represents the inverted comparison result of the original comparer.
*
* @template T - The type of the elements to compare.
* @param comparer - The original comparer function.
*
* @returns A new comparer function that represents the inverted comparison result of the original comparer.
*/
export function invertComparer<T>(comparer: Comparer<T>): Comparer<T> {
return (left, right) => comparer(right, left);
}
/**
* Combines two {@link Comparer} instances in order to create a new one that sorts
* elements based on the first comparer, and then by the second one.
*
* @template T - The type of the elements being compared.
* @param left - The first comparer to use when comparing elements.
* @param right - The second comparer to use when comparing elements.
*
* @returns A new {@link Comparer} instance that sorts elements based on the first comparer, and then by the second one.
*/
export function combineComparers<T>(left: Comparer<T>, right: Comparer<T>): Comparer<T> {
return (a, b) => {
const leftResult = left(a, b);
return leftResult === 0 ? right(a, b) : leftResult;
};
}

View file

@ -0,0 +1,134 @@
import { CALL, makeCallable } from "@/utils/functions/callable";
import { combineComparers, convertComparerToEqualityComparer, invertComparer } from "./comparer.utils";
import { CompositeEqualityComparer } from "./composite-equality-comparer";
import { Comparer } from "./comparer";
/**
* A class that represents a composite comparer.
*
* @template T - The type of the elements to compare.
*/
export interface CompositeComparer<T> extends Comparer<T> {
/**
* Compares two elements and returns a value indicating whether one element is less than, equal to, or greater than the other.
*
* @param left - The first element to compare.
* @param right - The second element to compare.
*
* @returns A number that represents the comparison result.
*/
(left: T, right: T): number;
}
/**
* A class that represents a composite comparer.
*
* @template T - The type of the elements to compare.
*/
export class CompositeComparer<T> {
/**
* The underlying comparer function used for comparison.
*/
private readonly _comparer: Comparer<T>;
/**
* The inverted version of this comparer.
*/
private _inverted?: CompositeComparer<T>;
/**
* Constructs a new instance of {@link CompositeComparer}.
*
* @param comparer - An underlying comparer that should be used for comparison.
* @param inverted - A cached inverted {@link CompositeComparer} instance, if any.
*
* @remarks
*
* Should **not** be called directly. Use {@link create}, or {@link createInternal} instead.
*/
private constructor(comparer: Comparer<T>, inverted: CompositeComparer<T>) {
this._comparer = comparer;
this._inverted = inverted;
}
/**
* Creates a new instance of {@link CompositeComparer}.
*
* @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 CompositeComparer} instance, if any.
*
* @returns A new instance of {@link CompositeComparer}.
*/
private static createInternal<T>(comparer: Comparer<T>, inverted?: CompositeComparer<T>): CompositeComparer<T> {
return makeCallable(new CompositeComparer(comparer, inverted));
}
/**
* Creates a new instance of {@link CompositeComparer}.
*
* @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 CompositeComparer}.
*/
static create<T>(comparer: Comparer<T>): CompositeComparer<T> {
return CompositeComparer.createInternal(comparer);
}
/**
* Compares two elements and returns a value indicating whether one element is less than, equal to, or greater than the other.
*
* @param left - The first element to compare.
* @param right - The second element to compare.
*
* @returns A number that represents the comparison result.
*/
compare(left: T, right: T): number {
return this._comparer(left, right);
}
/**
* Compares two elements and returns a value indicating whether one element is less than, equal to, or greater than the other.
*
* @param left - The first element to compare.
* @param right - The second element to compare.
*
* @returns A number that represents the comparison result.
*/
[CALL](left: T, right: T): number {
return this._comparer(left, right);
}
/**
* Creates a new comparer which compares elements using this comparer first, and then using the `nextComparer`.
*
* @param comparer - The next comparer to use if this comparer returns equal result.
*
* @returns A new comparer which compares elements using this comparer first, and then using the `nextComparer`.
*/
thenBy(comparer: Comparer<T>): CompositeComparer<T> {
const unwrappedComparer = comparer instanceof CompositeComparer ? comparer._comparer : comparer;
const combinedComparer = combineComparers(this._comparer, unwrappedComparer);
return CompositeComparer.createInternal(combinedComparer);
}
/**
* Creates a new comparer that inverts the comparison result of this comparer.
*
* @returns A new comparer that inverts the comparison result of this comparer.
*/
invert(): CompositeComparer<T> {
this._inverted ??= CompositeComparer.createInternal(invertComparer(this._comparer), this);
return this._inverted;
}
/**
* Converts the current {@link CompositeComparer} instance into a new {@link CompositeEqualityComparer} instance.
*
* @returns A new {@link CompositeEqualityComparer} instance that uses the underlying comparer function to compare for equality.
*/
asEqualityComparer(): CompositeEqualityComparer<T> {
return CompositeEqualityComparer.create(convertComparerToEqualityComparer(this._comparer));
}
}

View file

@ -0,0 +1,113 @@
import { CompositeComparer } from "@/utils/comparison/composite-comparer";
import {
createComparer,
combineComparers,
invertComparer,
convertComparerToEqualityComparer,
createBaseComparer,
createDefaultComparer,
} from "@/utils/comparison/comparer";
describe("createComparer", () => {
test("creates a CompositeComparer from the given comparer", () => {
const comparer = createComparer((a: number, b: number) => a - b);
expect(comparer).toBeInstanceOf(CompositeComparer);
});
});
describe("combineComparers", () => {
test("chains comparers in the right order", () => {
const firstCompare = (a: [number, string], b: [number, string]) => a[0] - b[0];
const secondCompare = (a: [number, string], b: [number, string]) => a[1].localeCompare(b[1]);
const comparer = combineComparers(firstCompare, secondCompare);
expect(comparer([1, "b"], [2, "a"])).toBeLessThan(0);
expect(comparer([2, "a"], [1, "b"])).toBeGreaterThan(0);
expect(comparer([1, "a"], [1, "b"])).toBeLessThan(0);
expect(comparer([1, "b"], [1, "a"])).toBeGreaterThan(0);
expect(comparer([1, "a"], [1, "a"])).toEqual(0);
});
});
describe("invertComparer", () => {
test("inverts comparisons", () => {
const comparer = invertComparer((a: number, b:number) => a - b);
expect(comparer(5, 3)).toBeLessThan(0);
expect(comparer(3, 5)).toBeGreaterThan(0);
expect(comparer(5, 5)).toEqual(0);
});
});
describe("convertComparerToEqualityComparer", () => {
test("returns an equality comparer that returns true when the original comparer would return 0", () => {
const comparer = convertComparerToEqualityComparer((a: number, b: number) => a - b);
expect(comparer(5, 5)).toEqual(true);
expect(comparer(3, 5)).toEqual(false);
expect(comparer(5, 3)).toEqual(false);
});
});
describe("createBaseComparer", () => {
test("treats undefined as smaller than any other value", () => {
const comparer = createBaseComparer();
expect(comparer(undefined, null)).toBeLessThan(0);
expect(comparer(undefined, 5)).toBeLessThan(0);
expect(comparer(undefined, "test")).toBeLessThan(0);
expect(comparer(undefined, undefined)).toEqual(0);
});
test("treats null as smaller than any other value except undefined", () => {
const comparer = createBaseComparer();
expect(comparer(null, undefined)).toBeGreaterThan(0);
expect(comparer(null, null)).toEqual(0);
expect(comparer(null, 5)).toBeLessThan(0);
expect(comparer(null, "test")).toBeLessThan(0);
});
test("treats any non-null and non-undefined values as equal", () => {
const comparer = createBaseComparer();
expect(comparer(5, 5)).toEqual(0);
expect(comparer("test", "test")).toEqual(0);
expect(comparer("test", 5)).toEqual(0);
expect(comparer({}, [])).toEqual(0);
});
});
describe("createDefaultComparer", () => {
test("compares two values using their natural order", () => {
const comparer = createDefaultComparer();
expect(comparer(5, 3)).toBeGreaterThan(0);
expect(comparer(3, 5)).toBeLessThan(0);
expect(comparer(5, 5)).toEqual(0);
expect(comparer("b", "a")).toBeGreaterThan(0);
expect(comparer("a", "b")).toBeLessThan(0);
expect(comparer("a", "a")).toEqual(0);
});
test("treats undefined as smaller than any other value", () => {
const comparer = createDefaultComparer();
expect(comparer(undefined, null)).toBeLessThan(0);
expect(comparer(undefined, 5)).toBeLessThan(0);
expect(comparer(undefined, "test")).toBeLessThan(0);
expect(comparer(undefined, undefined)).toEqual(0);
});
test("treats null as smaller than any other value except undefined", () => {
const comparer = createDefaultComparer();
expect(comparer(null, undefined)).toBeGreaterThan(0);
expect(comparer(null, null)).toEqual(0);
expect(comparer(null, 5)).toBeLessThan(0);
expect(comparer(null, "test")).toBeLessThan(0);
});
});

View file

@ -0,0 +1,66 @@
import { CompositeComparer } from "@/utils/comparison/composite-comparer";
describe("CompositeComparer", () => {
describe("create", () => {
test("creates a new instance from the given comparer", () => {
const comparer = CompositeComparer.create((a: number, b: number) => a - b);
expect(comparer).toBeInstanceOf(CompositeComparer);
});
});
describe("compare", () => {
test("compares two numbers using the original comparer", () => {
const comparer = CompositeComparer.create((a: number, b: number) => a - b);
expect(comparer.compare(5, 3)).toBeGreaterThan(0);
expect(comparer.compare(3, 5)).toBeLessThan(0);
expect(comparer.compare(5, 5)).toEqual(0);
});
});
describe("__invoke__", () => {
test("can be used as a function", () => {
const comparer = CompositeComparer.create((a: number, b: number) => a - b);
expect(comparer(5, 3)).toBeGreaterThan(0);
expect(comparer(3, 5)).toBeLessThan(0);
expect(comparer(5, 5)).toEqual(0);
});
});
describe("thenBy", () => {
test("chains comparers in the right order", () => {
const firstCompare = (a: [number, string], b: [number, string]) => a[0] - b[0];
const secondCompare = (a: [number, string], b: [number, string]) => a[1].localeCompare(b[1]);
const comparer = CompositeComparer.create(firstCompare).thenBy(secondCompare);
expect(comparer.compare([1, "b"], [2, "a"])).toBeLessThan(0);
expect(comparer.compare([2, "a"], [1, "b"])).toBeGreaterThan(0);
expect(comparer.compare([1, "a"], [1, "b"])).toBeLessThan(0);
expect(comparer.compare([1, "b"], [1, "a"])).toBeGreaterThan(0);
expect(comparer.compare([1, "a"], [1, "a"])).toEqual(0);
});
});
describe("invert", () => {
test("inverts comparisons", () => {
const comparer = CompositeComparer.create((a: number, b:number) => a - b).invert();
expect(comparer.compare(5, 3)).toBeLessThan(0);
expect(comparer.compare(3, 5)).toBeGreaterThan(0);
expect(comparer.compare(5, 5)).toEqual(0);
});
});
describe("asEqualityComparer", () => {
test("returns an equality comparer that returns true when the original comparer would return 0", () => {
const comparer = CompositeComparer.create((a: number, b: number) => a - b).asEqualityComparer();
expect(comparer(5, 5)).toEqual(true);
expect(comparer(3, 5)).toEqual(false);
expect(comparer(5, 3)).toEqual(false);
});
});
});