-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomparator.ts
186 lines (171 loc) · 5.25 KB
/
comparator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
* Defines the {@linkcode Comparator} interface, as well as providing various
* convenience functions for creating comparators out of data or other existing
* comparators.
* @packageDocumentation
*/
import {Scoring, scoringFromArray, scoringFromArrayUsingMap} from './scoring';
/**
* A [comparator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Description)
* is a function that takes two elements of the same type and returns a number
* that indicates the comparison result.
*
* @returns
* - `0` or `-0` if the two elements are considered equal.
* - Any number greater than `0` if the first element is greater than the second
* one.
* - Any number lesser than `0` if the first is lesser than the second.
*/
export interface Comparator<Element> {
(a: Element, b: Element): number;
}
/**
* Joins one or more existing comparators into a new comparator so that when a
* comparison results in equality, the next one is used as a fallback.
*
* @throws {`TypeError`} if no comparators are given
*
* @example
* ```ts
* const byName = (person1, person2) => byString(person1.name, person2.name);
* const byAge = (person1, person2) => byNumber(person1.age, person2.age);
*
* const byNameThenByAge = join(byName, byAge);
* ```
*/
export function join<Element>(
...comparators: Comparator<Element>[]
): Comparator<Element> {
switch (comparators.length) {
case 0:
throw new TypeError('`join` must be given at least one comparator.');
case 1:
return comparators[0];
default:
const result = function joinedComparator(a: Element, b: Element): number {
const lastIndex = comparators.length - 1;
const last = comparators[lastIndex];
const rest = comparators.slice(0, lastIndex);
for (const compare of rest) {
const result = compare(a, b);
if (result !== 0) {
return result;
}
}
return last(a, b);
};
const names = comparators.map(nameOfFunction).join(', ');
Object.defineProperty?.(result, 'name', {
value: `joinedComparator(${names})`,
});
return result;
}
}
/**
* Creates a reversed version of the given comparator.
*
* @example
* ```ts
* const byNumberDescending = reversed(byNumber);
* ```
*/
export function reversed<Element>(
comparator: Comparator<Element>,
): Comparator<Element> {
const result = function reversedComparator(a: Element, b: Element): number {
return -comparator(a, b);
};
Object.defineProperty?.(result, 'name', {
value: `reversed(${nameOfFunction(comparator)})`,
});
return result;
}
/**
* Creates a comparator that compares two elements based on their keys.
*
* The `keyOf` function is called repeatedly throughout the sorting process, so
* make sure that it is efficient. If it is computationally costly to calculate
* the key of an element, consider using a `Map` or a `WeakMap` to cache a key
* once it is calculated.
*
* @param keyOf a function that calculates the key of an element
* @param keyComparator a comparator that can compare keys returned by `keyOf`
*
* @example
* ```ts
* interface Person {
* name: string;
* age: number;
* }
*
* const byAge = keyed((person: Person) => person.age, byNumber);
* ```
*/
export function keyed<Element, Property>(
keyOf: (element: Element) => Property,
keyComparator: Comparator<Property>,
): Comparator<Element> {
const result = function keyedComparator(a: Element, b: Element): number {
return keyComparator(keyOf(a), keyOf(b));
};
return result;
}
/**
* Becomes `scoringFromArrayByMap` if `Map` is available from the environment.
* Otherwise falls back to `scoringFromArray`.
*/
const makeScoring = (() => {
try {
Map;
return scoringFromArrayUsingMap;
} catch (_) {
return scoringFromArray;
}
})();
/**
* Generates a {@linkcode Comparator} given an array of ordered elements or a
* scoring function {@linkcode Scoring}. This is useful for any sort of
* enum-like type, where a limited subset of strings and / or numbers are used
* for special meaning.
*
* When given an array, a scoring function is generated to look up from that
* array. This lookup process is at least linear, but becomes more efficient if
* a good implementation of `Map` is available in the environment. If the lookup
* fails because an unknown element not in the array was given, then it is given
* the lowest possible score of -1.
*
* @param scoring An array denoting the desired order of ranking, or a custom
* scoring function
*
* @example
* ```ts
* const byVowelCount = ranking<string>(word => word.match(/[aeiou]/gi).length);
*
* enum Rarity {
* Common,
* Uncommon,
* Rare,
* Legendary,
* Exotic,
* }
*
* const byRarity = ranking<Rarity>([
* Rarity.Common,
* Rarity.Uncommon,
* Rarity.Rare,
* Rarity.Legendary,
* Rarity.Exotic,
* ]);
* ```
*/
export function ranking<Element>(
scoring: Element[] | Scoring<Element>,
): Comparator<Element> {
const scoreOf = scoring instanceof Function ? scoring : makeScoring(scoring);
return function rankingComparator(a: Element, b: Element): number {
return scoreOf(a) - scoreOf(b);
};
}
function nameOfFunction(f: Function & {displayName?: string}): string {
return f?.displayName ?? f.name;
}