mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-25 18:01:07 -05:00
Implemented comparers
This commit is contained in:
parent
a537525099
commit
d1b1ed9a16
5 changed files with 449 additions and 0 deletions
93
src/utils/comparison/comparer.ts
Normal file
93
src/utils/comparison/comparer.ts
Normal 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>;
|
||||||
|
}
|
43
src/utils/comparison/comparer.utils.ts
Normal file
43
src/utils/comparison/comparer.utils.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
134
src/utils/comparison/composite-comparer.ts
Normal file
134
src/utils/comparison/composite-comparer.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
113
tests/unit/utils/comparison/comparer.spec.ts
Normal file
113
tests/unit/utils/comparison/comparer.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
66
tests/unit/utils/comparison/composite-comparer.spec.ts
Normal file
66
tests/unit/utils/comparison/composite-comparer.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue