/** * Comparable is a structure that has an idea of comparability. Comparability * means that two of the same type of object can be compared such that the * condition of comparison can be true or false. The canonical comparison is * equality. * * @module Comparable * @since 2.0.0 */ import type { Hold, In, Kind, Out, Spread } from "./kind.ts"; import type { Literal, Schemable } from "./schemable.ts"; import { handleThrow, identity, memoize, uncurry2 } from "./fn.ts"; type ReadonlyRecord = Readonly>; /** * The compare function in a Comparable. * * @since 2.0.0 */ export type Compare = (second: A) => (first: A) => boolean; /** * Extract the type parameter from a Comparable. * * @since 2.0.0 */ export type TypeOf = U extends Comparable ? A : never; /** * A Comparable is an algebra with a notion of equality. Specifically, * a Comparable for a type T has an equal method that determines if the * two objects are the same. Comparables can be combined, like many * algebraic structures. The combinators for Comparable in fun can be found * in [comparable.ts](./comparable.ts). * * An instance of a Comparable must obey the following laws: * * 1. Reflexivity: compare(a, a) === true * 2. Symmetry: compare(a, b) === compare(b, a) * 3. Transitivity: if compare(a, b) and compare(b, c), then compare(a, c) * * @since 2.0.0 */ export interface Comparable extends Hold { readonly compare: Compare; } /** * Specifies Comparable as a Higher Kinded Type, with * covariant parameter A corresponding to the 0th * index of any Substitutions. * * @since 2.0.0 */ export interface KindComparable extends Kind { readonly kind: Comparable>; } /** * Specifies Comparable as a Higher Kinded Type, with * contravariant parameter D corresponding to the 0th * index of any Substitutions. * * @since 2.0.0 */ export interface KindContraComparable extends Kind { readonly kind: Comparable>; } /** * Create a Comparable from a Compare function. * * @example * ```ts * import { fromCompare } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * const { compare } = fromCompare( * (second) => (first) => first === second * ); * * const result = compare(1)(1); // true * ``` * * @since 2.0.0 */ export function fromCompare( compare: Compare = (second) => (first) => first === second, ): Comparable { return { compare }; } /** * A single instance of Comparable that uses strict equality for comparison. * * @since 2.0.0 */ // deno-lint-ignore no-explicit-any const STRICT_EQUALITY: Comparable = fromCompare(); /** * Create a Comparable for Readonly types. * * @example * ```ts * import { readonly, fromCompare } from "./comparable.ts"; * * // This has type Comparable> * const ComparableMutableArray = fromCompare>( * (second) => (first) => first.length === second.length * && first.every((value, index) => value === second[index]) * ); * * // This has type Comparable>> * const ComparableReadonlyArray = readonly(ComparableMutableArray); * ``` * * @since 2.0.0 */ export function readonly( comparable: Comparable, ): Comparable> { return comparable; } /** * A Comparable that can compare any unknown values (and thus can * compare any values). Underneath it uses strict equality * for the comparison. * * @example * ```ts * import { unknown } from "./comparable.ts"; * * const result1 = unknown.compare(1)("Hello"); // false * const result2 = unknown.compare(1)(1); // true * ``` * * @since 2.0.0 */ export const unknown: Comparable = STRICT_EQUALITY; /** * A Comparable that compares strings using strict equality. * * @example * ```ts * import { string } from "./comparable.ts"; * * const result1 = string.compare("World")("Hello"); // false * const result2 = string.compare("")(""); // true * ``` * * @since 2.0.0 */ export const string: Comparable = STRICT_EQUALITY; /** * A Comparable that compares number using strict equality. * * @example * ```ts * import { number } from "./comparable.ts"; * * const result1 = number.compare(1)(2); // false * const result2 = number.compare(1)(1); // true * ``` * * @since 2.0.0 */ export const number: Comparable = STRICT_EQUALITY; /** * A Comparable that compares booleans using strict equality. * * @example * ```ts * import { boolean } from "./comparable.ts"; * * const result1 = boolean.compare(true)(false); // false * const result2 = boolean.compare(true)(true); // true * ``` * * @since 2.0.0 */ export const boolean: Comparable = STRICT_EQUALITY; /** * Creates a Comparable that compares a union of literals * using strict equality. * * @example * ```ts * import { literal } from "./comparable.ts"; * * const { compare } = literal(1, 2, "Three"); * * const result1 = compare(1)("Three"); // false * const result2 = compare("Three")("Three"); // true * ``` * * @since 2.0.0 */ export function literal( ..._: A ): Comparable { return STRICT_EQUALITY; } /** * Create a Comparable for nullable types. * * @example * ```ts * import { nullable } from "./comparable.ts"; * import * as N from "./number.ts"; * * const nullableComparable = nullable(N.ComparableNumber); * const result1 = nullableComparable.compare(5)(5); // true * const result2 = nullableComparable.compare(null)(null); // true * const result3 = nullableComparable.compare(5)(null); // false * ``` * * @since 2.0.0 */ export function nullable({ compare }: Comparable): Comparable { return fromCompare((second) => (first) => first === null || second === null ? first === second : compare(second)(first) ); } /** * Create a Comparable for undefined types. * * @example * ```ts * import { undefinable } from "./comparable.ts"; * import * as N from "./number.ts"; * * const undefinableComparable = undefinable(N.ComparableNumber); * const result1 = undefinableComparable.compare(5)(5); // true * const result2 = undefinableComparable.compare(undefined)(undefined); // true * const result3 = undefinableComparable.compare(5)(undefined); // false * ``` * * @since 2.0.0 */ export function undefinable( { compare }: Comparable, ): Comparable { return fromCompare((second) => (first) => first === undefined || second === undefined ? first === second : compare(second)(first) ); } /** * Create a Comparable for record types. * * @example * ```ts * import { record } from "./comparable.ts"; * import * as N from "./number.ts"; * * const recordComparable = record(N.ComparableNumber); * const result = recordComparable.compare({ a: 1, b: 2 })({ a: 1, b: 2 }); // true * ``` * * @since 2.0.0 */ export function record(eq: Comparable): Comparable> { const isSub = (first: ReadonlyRecord, second: ReadonlyRecord) => { for (const key in first) { if (!Object.hasOwn(second, key) || !eq.compare(second[key])(first[key])) { return false; } } return true; }; return fromCompare((second) => (first) => isSub(first, second) && isSub(second, first) ); } /** * Create a Comparable for array types. * * @example * ```ts * import { array } from "./comparable.ts"; * import * as N from "./number.ts"; * * const arrayComparable = array(N.ComparableNumber); * const result = arrayComparable.compare([1, 2, 3])([1, 2, 3]); // true * ``` * * @since 2.0.0 */ export function array( { compare }: Comparable, ): Comparable> { return fromCompare((second) => (first) => Array.isArray(first) && Array.isArray(second) && first.length === second.length && first.every((value, index) => compare(second[index])(value)) ); } /** * Create a Comparable for tuple types. * * @example * ```ts * import { tuple } from "./comparable.ts"; * import * as N from "./number.ts"; * import * as S from "./string.ts"; * * const tupleComparable = tuple(N.ComparableNumber, S.ComparableString); * const result = tupleComparable.compare([1, "hello"])([1, "hello"]); // true * ``` * * @since 2.0.0 */ // deno-lint-ignore no-explicit-any export function tuple>>( ...comparables: T ): Comparable< { [K in keyof T]: T[K] extends Comparable ? A : never } > { return fromCompare((second) => (first) => Array.isArray(first) && Array.isArray(second) && first.length === second.length && comparables.every(({ compare }, index) => compare(second[index])(first[index]) ) ); } /** * Create a Comparable for struct types. * * @example * ```ts * import { struct } from "./comparable.ts"; * import * as N from "./number.ts"; * import * as S from "./string.ts"; * * const structComparable = struct({ * id: N.ComparableNumber, * name: S.ComparableString * }); * const result = structComparable.compare({ id: 1, name: "John" })({ id: 1, name: "John" }); // true * ``` * * @since 2.0.0 */ export function struct( comparables: { readonly [K in keyof A]: Comparable }, ): Comparable<{ readonly [K in keyof A]: A[K] }> { const _comparables = Object.entries(comparables) as [ keyof A, Comparable, ][]; return fromCompare((second) => (first) => _comparables.every(([key, { compare }]) => compare(second[key])(first[key])) ); } /** * Create a Comparable for partial types. * * @example * ```ts * import { partial } from "./comparable.ts"; * import * as N from "./number.ts"; * import * as S from "./string.ts"; * * const partialComparable = partial({ * id: N.ComparableNumber, * name: S.ComparableString * }); * const result = partialComparable.compare({ id: 1 })({ id: 1 }); // true * ``` * * @since 2.0.0 */ export function partial( comparables: { readonly [K in keyof A]: Comparable }, ): Comparable<{ readonly [K in keyof A]?: A[K] }> { const _comparables = Object.entries(comparables) as [ keyof A, Comparable, ][]; return fromCompare((second) => (first) => _comparables.every(([key, { compare }]) => { if (key in first && key in second) { return compare(second[key] as A[keyof A])(first[key] as A[keyof A]); } return first[key] === second[key]; }) ); } /** * Create a Comparable for intersection types. * * @example * ```ts * import { intersect, struct, partial, string } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * const { compare } = pipe( * struct({ firstName: string }), * intersect(partial({ lastName: string })) * ); * * const batman = { firstName: "Batman" }; * const grace = { firstName: "Grace", lastName: "Hopper" }; * * const result1 = compare(batman)(grace); // false * const result2 = compare(grace)(grace); // true * ``` * * @since 2.0.0 */ export function intersect( second: Comparable, ): (first: Comparable) => Comparable> { return (first: Comparable): Comparable> => fromCompare((snd: A & I) => (fst: A & I) => first.compare(snd)(fst) && second.compare(snd)(fst) ) as Comparable>; } /** * Create a Comparable from two other Comparables. The resultant Comparable checks * that any two values are equal according to at least one of the supplied * eqs. * * It should be noted that we cannot differentiate the eq used to * compare two disparate types like number and number[]. Thus, internally * union must type cast to any and treat thrown errors as a false * equivalence. * * @example * ```ts * import { union, number, string } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * const { compare } = pipe(number, union(string)); * * const result1 = compare(1)("Hello"); // false * const result2 = compare(1)(1); // true * ``` * * @since 2.0.0 */ export function union( second: Comparable, ): (first: Comparable) => Comparable { return (first: Comparable) => { const _first = handleThrow( uncurry2(first.compare), identity, () => false, ); const _second = handleThrow( uncurry2(second.compare), identity, () => false, ); // deno-lint-ignore no-explicit-any return fromCompare((snd: any) => (fst: any) => _first(fst, snd) || _second(fst, snd) ); }; } /** * Create a lazy Comparable that defers evaluation. * * @example * ```ts * import { lazy, intersect, struct, partial, string, Comparable } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * type Person = { name: string, child?: Person }; * * // Annotating the type is required for recursion * const person: Comparable = lazy('Person', () => pipe( * struct({ name: string }), * intersect(partial({ child: person })) * )); * * const icarus = { name: "Icarus" }; * const daedalus = { name: "Daedalus", child: icarus }; * * const result1 = person.compare(icarus)(daedalus); // false * const result2 = person.compare(daedalus)(daedalus); // true * ``` * * @since 2.0.0 */ export function lazy(_: string, f: () => Comparable): Comparable { const memo = memoize>(f); return fromCompare((second) => (first) => memo().compare(second)(first)); } /** * Create a Comparable for thunk types. * * @example * ```ts * import { struct, thunk, number } from "./comparable.ts"; * * const { compare } = struct({ asNumber: thunk(number) }); * * const one = { asNumber: () => 1 }; * const two = { asNumber: () => 2 }; * * const result1 = compare(one)(two); // false * const result2 = compare(one)(one); // true * ``` * * @since 2.0.0 */ export function thunk({ compare }: Comparable): Comparable<() => A> { return fromCompare((second) => (first) => compare(second())(first())); } /** * Create a Comparable for method types. * * @example * ```ts * import { method, number } from "./comparable.ts"; * * // This eq will work for date, but also for any objects that have * // a valueOf method that returns a number. * const date = method("valueOf", number); * * const now = new Date(); * const alsoNow = new Date(Date.now()); * const later = new Date(Date.now() + 60 * 60 * 1000); * * const result1 = date.compare(now)(alsoNow); // true * const result2 = date.compare(now)(later); // false * ``` * * @since 2.0.0 */ export function method( method: M, { compare }: Comparable, ): Comparable<{ readonly [K in M]: () => A }> { return fromCompare((second) => (first) => compare(second[method]())(first[method]()) ); } /** * Create a Comparable using a Comparable and a function that takes * a type L and returns a type D. * * @example * ```ts * import { premap, number } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * const dateToNumber = (d: Date): number => d.valueOf(); * const date = pipe(number, premap(dateToNumber)); * * const now = new Date(); * const alsoNow = new Date(Date.now()); * const later = new Date(Date.now() + 60 * 60 * 1000); * * const result1 = date.compare(now)(alsoNow); // true * const result2 = date.compare(now)(later); // false * ``` * * Another use for premap with eq is to check for * equality after normalizing data. In the following we * can compare strings ignoring case by normalizing to * lowercase strings. * * @example * ```ts * import { premap, string } from "./comparable.ts"; * import { pipe } from "./fn.ts"; * * const lowercase = (s: string) => s.toLowerCase(); * const insensitive = pipe( * string, // exact string compare * premap(lowercase), // makes all strings lowercase * ); * * const result1 = insensitive.compare("Hello")("World"); // false * const result2 = insensitive.compare("hello")("Hello"); // true * ``` * * @since 2.0.0 */ export function premap( fld: (l: L) => D, ): (eq: Comparable) => Comparable { return ({ compare }) => fromCompare((second) => (first) => compare(fld(second))(fld(first))); } /** * The canonical implementation of Schemable for a Comparable. It contains * the methods unknown, string, number, boolean, literal, nullable, * undefinable, record, array, tuple, struct, partial, intersect, * union, and lazy. * * @since 2.0.0 */ export const SchemableComparable: Schemable = { unknown: () => unknown, string: () => string, number: () => number, boolean: () => boolean, literal, nullable, undefinable, record, array, tuple, struct, partial, intersect, union, lazy, };