@rimbu/common
Version:
Common types and objects used in many other Rimbu packages
626 lines (549 loc) • 18.1 kB
text/typescript
import { Eq } from './internal.mjs';
/**
* An object providing methods to compare two values of type `K`.
* @typeparam K - the value type
*/
export interface Comp<K> {
/**
* Returns 0 if given `value1` and `value2` are equal, a positive value is `value1` is greater than
* `value2`, and a negative value otherwise.
* @param value1 - the first value to compare
* @param value2 - the seconds value to compare
* @example
* ```ts
* const c = Comp.numberComp()
* console.log(c.compare(5, 5))
* // => 0
* console.log(c.compare(3, 5))
* // => -2
* console.log(c.compare(5, 3))
* // => 2
* ```
*/
compare(value1: K, value2: K): number;
/**
* Returns true if this instance can compare given `obj`.
* @param obj - the object to check
* @example
* ```ts
* const c = Comp.numberComp()
* console.log(c.isComparable(5))
* // => true
* console.log(c.isComparable('a'))
* // => false
* ```
*/
isComparable(obj: any): obj is K;
}
export namespace Comp {
const _anyFlatComp: Comp<any> = createAnyComp('FLAT');
const _anyShallowComp: Comp<any> = createAnyComp('SHALLOW');
const _anyDeepComp: Comp<any> = createAnyComp('DEEP');
/**
* Returns the default Comp instance, which is the Comp.anyDeepComp() instance.
*/
export function defaultComp(): Comp<any> {
return _anyDeepComp;
}
const _numberComp: Comp<number> = {
isComparable(obj): obj is number {
return typeof obj === 'number';
},
compare(v1, v2) {
if (Number.isFinite(v1) && Number.isFinite(v2)) {
return v1 - v2;
}
if (Number.isNaN(v1)) {
if (Number.isNaN(v2)) return 0;
if (v2 === Number.POSITIVE_INFINITY) return 1;
if (v2 === Number.NEGATIVE_INFINITY) return -1;
return -1;
}
// only infinities remain
if (v1 === Number.POSITIVE_INFINITY) {
return v2 === Number.POSITIVE_INFINITY ? 0 : 1;
}
// v1 === Number.NEGATIVE_INFINITY
return v2 === Number.NEGATIVE_INFINITY ? 0 : -1;
},
};
/**
* Returns a default number Comp instance that orders numbers naturally.
* @example
* ```ts
* const c = Comp.numberComp();
* console.log(c.compare(3, 5))
* // => -2
* ```
*/
export function numberComp(): Comp<number> {
return _numberComp;
}
const _booleanComp: Comp<boolean> = {
isComparable(obj): obj is boolean {
return typeof obj === 'boolean';
},
compare(v1, v2) {
return v1 === v2 ? 0 : v1 ? 1 : -1;
},
};
/**
* Returns a default boolean Comp instance that orders booleans according to false < true.
* @example
* ```ts
* const c = Comp.booleanComp();
* console.log(c.compare(false, true) < 0)
* // => true
* console.log(c.compare(true, true))
* // => 0
* ```
*/
export function booleanComp(): Comp<boolean> {
return _booleanComp;
}
const _bigIntComp: Comp<bigint> = {
isComparable(obj): obj is bigint {
return typeof obj === 'bigint';
},
compare(v1, v2) {
const res = v1 - v2;
if (res > 0) return 1;
if (res < 0) return -1;
return 0;
},
};
/**
* Returns a default bigint Comp instance that orders bigint numbers naturally.
*/
export function bigIntComp(): Comp<bigint> {
return _bigIntComp;
}
const _defaultCollator = Intl.Collator('und');
const _stringComp: Comp<string> = {
isComparable(obj): obj is string {
return typeof obj === 'string';
},
compare: _defaultCollator.compare,
};
const _anyStringJSONComp: Comp<any> = {
isComparable(obj): obj is any {
return true;
},
compare(v1, v2) {
return _defaultCollator.compare(JSON.stringify(v1), JSON.stringify(v2));
},
};
/**
* Returns a Comp instance converts values to string with JSON.stringify, and orders the resulting string naturally.
*/
export function anyStringJSONComp<T>(): Comp<T> {
return _anyStringJSONComp;
}
/**
* Returns a `Comp` instance that compares strings based on the string's `localeCompare` method.
* @param locales - (optional) a locale or list of locales
* @param options - (optional) see String.localeCompare for details
*/
export function stringComp(
...args: ConstructorParameters<typeof Intl.Collator>
): Comp<string> {
if (args.length === 0) return _stringComp;
const collator = Intl.Collator(...args);
return {
isComparable(obj): obj is string {
return typeof obj === 'string';
},
compare: collator.compare,
};
}
/**
* Returns a `Comp` instance that compares strings in a case-insensitive way.
*/
export function stringCaseInsensitiveComp(): Comp<string> {
return stringComp('und', { sensitivity: 'accent' });
}
const _stringCharCodeComp: Comp<string> = {
isComparable(obj: any): obj is any {
return typeof obj === 'string';
},
compare(v1: any, v2: any): number {
const len = Math.min(v1.length, v2.length);
let i = -1;
while (++i < len) {
const diff = v1.charCodeAt(i) - v2.charCodeAt(i);
if (diff !== 0) return diff;
}
return v1.length - v2.length;
},
};
/**
* Returns a string Comp instance that orders strings according to their indexed char codes.
*/
export function stringCharCodeComp(): Comp<string> {
return _stringCharCodeComp;
}
const _anyToStringComp: Comp<any> = {
isComparable(obj: any): obj is any {
return true;
},
compare(v1: any, v2: any): number {
return _defaultCollator.compare(
Eq.convertAnyToString(v1),
Eq.convertAnyToString(v2)
);
},
};
/**
* Returns a any Comp instance that orders any according to their toString values.
*/
export function anyToStringComp(): Comp<any> {
return _anyToStringComp;
}
/**
* Returns a Comp instance that orders objects with a `valueOf` method according to the given `valueComp` instance for the valueOf values.
* @param cls - the constructor of the values the Comp instance can compare
* @param valueComp - (optional) the Comp instance to use on the .valueOf values
*/
export function createValueOfComp<T extends { valueOf(): V }, V>(
cls: {
new (): T;
},
valueComp: Comp<V> = anyShallowComp()
): Comp<T> {
return {
isComparable(obj): obj is T {
return obj instanceof cls;
},
compare(v1, v2): number {
return valueComp.compare(v1.valueOf(), v2.valueOf());
},
};
}
const _DateComp: Comp<Date> = createValueOfComp(Date, _numberComp);
/**
* Returns a Date Comp instance that orders Dates according to their `.valueOf` value.
*/
export function dateComp(): Comp<Date> {
return _DateComp;
}
function createIterableComp<T>(itemComp: Comp<T>): Comp<Iterable<T>> {
return {
isComparable(obj): obj is Iterable<T> {
// unfortunately we cannot check element compatability
return (
typeof obj === 'object' && obj !== null && Symbol.iterator in obj
);
},
compare(v1, v2): number {
const iter1 = v1[Symbol.iterator]();
const iter2 = v2[Symbol.iterator]();
while (true) {
const value1 = iter1.next();
const value2 = iter2.next();
if (value1.done) return value2.done ? 0 : -1;
if (value2.done) return 1;
const result = itemComp.compare(value1.value, value2.value);
if (result !== 0) return result;
}
},
};
}
const _iterableAnyComp: Comp<Iterable<any>> = createIterableComp(
Comp.defaultComp()
);
/**
* Returns a Comp instance for Iterable objects that orders the Iterables by comparing the elements with the given `itemComp` Comp instance.
* @param itemComp - (optional) the Comp instance to use to compare the Iterable's elements.
* @example
* ```ts
* const c = Comp.iterableComp();
* console.log(c.compare([1, 3, 2], [1, 3, 2]))
* // => 0
* console.log(c.compare([1, 2, 3, 4], [1, 3, 2]) < 0)
* // => true
* ```
*/
export function iterableComp<T>(itemComp?: Comp<T>): Comp<Iterable<T>> {
if (undefined === itemComp) return _iterableAnyComp;
return createIterableComp(itemComp);
}
const _BooleanComp = createValueOfComp(Boolean, _booleanComp);
const _NumberComp = createValueOfComp(Number, _numberComp);
const _StringComp = createValueOfComp(String, _stringComp);
const _wrappedComps: Comp<unknown>[] = [
_BooleanComp,
_DateComp,
_NumberComp,
_StringComp,
];
function tryWrappedComp(v1: any, v2: any): number | undefined {
let i = -1;
const len = _wrappedComps.length;
while (++i < len) {
const comp = _wrappedComps[i];
if (comp.isComparable(v1) && comp.isComparable(v2)) {
return comp.compare(v1, v2);
}
}
return undefined;
}
function createObjectComp(
keyComp: Comp<any> = anyFlatComp(),
valueComp: Comp<any> = defaultComp()
): Comp<Record<any, any>> {
return {
isComparable(obj): obj is Record<any, any> {
return true;
},
compare(v1, v2): number {
const keys1 = Object.keys(v1);
const keys2 = Object.keys(v2);
if (keys1.length === 0) {
return keys2.length === 0 ? 0 : -1;
}
if (keys2.length === 0) {
return keys1.length === 0 ? 0 : 1;
}
keys1.sort(keyComp.compare);
keys2.sort(keyComp.compare);
const length = Math.min(keys1.length, keys2.length);
for (let index = 0; index < length; index++) {
const key1 = keys1[index];
const key2 = keys2[index];
const keyResult = keyComp.compare(key1, key2);
if (keyResult !== 0) return keyResult;
const value1 = v1[key1];
const value2 = v2[key2];
const valueResult = valueComp.compare(value1, value2);
if (valueResult !== 0) return valueResult;
}
const keyDiff = keys1.length - keys2.length;
return keyDiff;
},
};
}
const _objectAnyComp: Comp<Record<any, any>> = createObjectComp();
/**
* Returns a Comp instance for objects that orders the object keys according to the given `keyComp`, and then compares the corresponding
* values using the given `valueComp`. Objects are then compared as follows:<br/>
* starting with the smallest key of either object:<br/>
* - if only one of the objects has the key, the object with the key is considered to be larger than the other<br/>
* - if both objects have the key, the values are compared with `valueComp`. If the values are not equal, this result is returned.<br/>
*
* if the objects have the same keys with the same values, they are considered equal<br/>
* @param keyComp - (optional) the Comp instance used to order the object keys
* @param valueComp - (optional) the Comp instance used to order the object values
* @example
* ```ts
* const c = Comp.objectComp();
* console.log(c.compare({ a: 1 }, { a: 1 }))
* // => 0
* console.log(c.compare({ a: 1 }, { a: 2 }) < 0)
* // => true
* console.log(c.compare({ b: 5 }, { a: 2 }) < 0)
* // => true
* console.log(c.compare({ a: 1, b: 2 }, { b: 5 }) < 0)
* // => true
* console.log(c.compare({ a: 1, b: 2 }, { b: 2, a: 1 }))
* // => 0
* ```
*/
export function objectComp(options?: {
keyComp?: Comp<any>;
valueComp?: Comp<any>;
}): Comp<Record<any, any>> {
if (undefined === options) return _objectAnyComp;
return createObjectComp(options.keyComp, options.valueComp);
}
function createAnyComp(mode: 'FLAT' | 'SHALLOW' | 'DEEP'): Comp<any> {
return {
isComparable(obj): obj is any {
return true;
},
compare(v1, v2): number {
if (Object.is(v1, v2)) return 0;
const type1 = typeof v1;
const type2 = typeof v2;
if (type1 !== type2) {
// we can only compare different types though strings
return _anyToStringComp.compare(v1, v2);
}
switch (type1) {
case 'bigint':
return _bigIntComp.compare(v1, v2);
case 'boolean':
return _booleanComp.compare(v1, v2);
case 'number':
return _numberComp.compare(v1, v2);
case 'string':
return _stringComp.compare(v1, v2);
case 'object': {
if (null === v1) {
if (null === v2) return 0;
return -1;
}
if (null === v2) {
return 1;
}
const wrappedComp = tryWrappedComp(v1, v2);
if (undefined !== wrappedComp) return wrappedComp;
if (mode !== 'FLAT') {
if (
_iterableAnyComp.isComparable(v1) &&
_iterableAnyComp.isComparable(v2)
) {
if (mode === 'SHALLOW') {
return iterableComp(_anyFlatComp).compare(v1, v2);
}
return iterableComp(this).compare(v1, v2);
}
if (mode === 'SHALLOW') {
return createObjectComp(_anyFlatComp, _anyFlatComp).compare(
v1,
v2
);
}
return _objectAnyComp.compare(v1, v2);
}
}
}
return _anyToStringComp.compare(v1, v2);
},
};
}
/**
* Returns a Comp instance that compares any value using default comparison functions, but never recursively compares
* Iterables or objects. In those cases, it will use the stringComp instance.
* @example
* ```ts
* const c = Comp.anyFlatComp();
* console.log(c.compare({ a: 1, b: 1 }, { b: 1, a: 1 }) < 0)
* // => true
* // First object is smaller because the objects are converted to a string with and then compares the resulting string.
* ```
*/
export function anyFlatComp<T>(): Comp<T> {
return _anyFlatComp;
}
/**
* Returns a Comp instance that compares any value using default comparison functions. For Iterables and objects, their elements are compared
* only one level deep for performance and to avoid infinite recursion.
* @example
* ```ts
* const c = Comp.anyShallowComp();
* console.log(c.compare({ a: 1, b: 1 }, { b: 1, a: 1 }))
* // => 0
* console.log(c.compare([{ a: 1, b: 1 }], [{ b: 1, a: 1 }]) < 0)
* // => true
* // First object is smaller because the objects are converted to a string and then compares the resulting string.
* ```
*/
export function anyShallowComp<T>(): Comp<T> {
return _anyShallowComp;
}
/**
* Returns a Comp instance that compares any value using default comparison functions. For Iterables and objects, their elements are compared
* recursively.
* @note can become slow with large nested arrays and objects, and circular structures can cause infinite loops
* @example
* ```ts
* const c = Comp.anyDeepComp();
* console.log(c.compare({ a: 1, b: 1 }, { b: 1, a: 1 }))
* // => 0
* console.log(c.compare([{ a: 1, b: 1 }], [{ b: 1, a: 1 }]))
* // => 0
* ```
*/
export function anyDeepComp<T>(): Comp<T> {
return _anyDeepComp;
}
/**
* Returns a Comp instance that extends the given `comp` instance with the capability to handle `undefined` values, where undefined is considered to be smaller
* than any other value, and equal to another undefined.
* @param comp - the Comp instance to wrap
* @example
* ```ts
* const c = Comp.withUndefined(Comp.numberComp())
* console.log(c.compare(undefined, 5) < 0)
* // => true
* console.log(c.compare(undefined, undefined))
* // => 0
* ```
*/
export function withUndefined<T>(comp: Comp<T>): Comp<T | undefined> {
return {
isComparable(obj): obj is T | undefined {
return undefined === obj || comp.isComparable(obj);
},
compare(v1, v2): number {
if (undefined === v1) {
if (undefined === v2) return 0;
return -1;
}
if (undefined === v2) return 1;
return comp.compare(v1, v2);
},
};
}
/**
* Returns a Comp instance that extends the given `comp` instance with the capability to handle `null` values, where null is considered to be smaller
* than any other value, and equal to another null.
* @param comp - the Comp instance to wrap
* @example
* ```ts
* const c = Comp.withNull(Comp.numberComp())
* console.log(c.compare(null, 5) < 0)
* // => true
* console.log(c.compare(null, null))
* // => 0
* ```
*/
export function withNull<T>(comp: Comp<T>): Comp<T | null> {
return {
isComparable(obj): obj is T | null {
return null === obj || comp.isComparable(obj);
},
compare(v1, v2): number {
if (null === v1) {
if (null === v2) return 0;
return -1;
}
if (null === v2) return 1;
return comp.compare(v1, v2);
},
};
}
/**
* Returns a Comp instance the reverses the order of the given `comp` instance.
* @param comp - the Comp instance to wrap
* @example
* ```ts
* const c = Comp.invert(Comp.numberComp())
* console.log(c.compare(3, 5) > 0)
* // => true
* console.log(c.compare(5, 5))
* // => 0
* ```
*/
export function invert<T>(comp: Comp<T>): Comp<T> {
return {
compare(v1, v2): number {
return comp.compare(v2, v1);
},
isComparable: comp.isComparable,
};
}
/**
* Returns an `Eq` equality instance thet will return true when the given `comp` comparable instance returns 0.
* @param comp - the `Comp` comparable instance to convert
* @example
* ```ts
* const eq = Comp.toEq(Comp.objectComp())
* console.log(eq({ a: 1, b: 2 }, { b: 2, a: 1 }))
* // => true
* ```
*/
export function toEq<T>(comp: Comp<T>): Eq<T> {
return (v1: T, v2: T): boolean => comp.compare(v1, v2) === 0;
}
}