@oscarpalmer/atoms 0.192.0 → 0.193.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +25 -23
- package/dist/index.mjs +30 -31
- package/dist/queue.d.mts +25 -23
- package/dist/queue.mjs +27 -28
- package/package.json +3 -4
- package/src/array/difference.ts +0 -73
- package/src/array/exists.ts +0 -93
- package/src/array/filter.ts +0 -182
- package/src/array/find.ts +0 -183
- package/src/array/first.ts +0 -196
- package/src/array/flatten.ts +0 -20
- package/src/array/from.ts +0 -129
- package/src/array/get.ts +0 -131
- package/src/array/group-by.ts +0 -369
- package/src/array/index.ts +0 -26
- package/src/array/insert.ts +0 -52
- package/src/array/intersection.ts +0 -72
- package/src/array/last.ts +0 -194
- package/src/array/match.ts +0 -436
- package/src/array/move.ts +0 -315
- package/src/array/partition.ts +0 -91
- package/src/array/push.ts +0 -23
- package/src/array/reverse.ts +0 -33
- package/src/array/select.ts +0 -260
- package/src/array/single.ts +0 -93
- package/src/array/slice.ts +0 -335
- package/src/array/sort.ts +0 -607
- package/src/array/splice.ts +0 -106
- package/src/array/swap.ts +0 -322
- package/src/array/to-map.ts +0 -371
- package/src/array/to-record.ts +0 -351
- package/src/array/to-set.ts +0 -80
- package/src/array/toggle.ts +0 -79
- package/src/array/union.ts +0 -72
- package/src/array/unique.ts +0 -67
- package/src/array/update.ts +0 -75
- package/src/beacon.ts +0 -378
- package/src/color/constants.ts +0 -137
- package/src/color/index.ts +0 -83
- package/src/color/instance.ts +0 -162
- package/src/color/misc/alpha.ts +0 -56
- package/src/color/misc/format.ts +0 -29
- package/src/color/misc/get.ts +0 -210
- package/src/color/misc/is.ts +0 -221
- package/src/color/misc/state.ts +0 -243
- package/src/color/models.ts +0 -389
- package/src/color/space/hex.ts +0 -184
- package/src/color/space/hsl.ts +0 -174
- package/src/color/space/hwb.ts +0 -153
- package/src/color/space/rgb.ts +0 -240
- package/src/function/assert.ts +0 -174
- package/src/function/index.ts +0 -2
- package/src/function/limit.ts +0 -83
- package/src/function/memoize.ts +0 -202
- package/src/function/once.ts +0 -200
- package/src/function/retry.ts +0 -175
- package/src/function/work.ts +0 -1007
- package/src/herald.ts +0 -216
- package/src/index.ts +0 -68
- package/src/internal/array/callbacks.ts +0 -46
- package/src/internal/array/chunk.ts +0 -51
- package/src/internal/array/compact.ts +0 -56
- package/src/internal/array/find.ts +0 -200
- package/src/internal/array/group.ts +0 -42
- package/src/internal/array/index-of.ts +0 -182
- package/src/internal/array/insert.ts +0 -80
- package/src/internal/array/overlap.ts +0 -35
- package/src/internal/array/sets.ts +0 -74
- package/src/internal/array/shuffle.ts +0 -33
- package/src/internal/array/update.ts +0 -52
- package/src/internal/function/misc.ts +0 -8
- package/src/internal/function/timer.ts +0 -168
- package/src/internal/is.ts +0 -244
- package/src/internal/math/aggregate.ts +0 -164
- package/src/internal/math/misc.ts +0 -28
- package/src/internal/number.ts +0 -129
- package/src/internal/random.ts +0 -49
- package/src/internal/result.ts +0 -92
- package/src/internal/sized.ts +0 -25
- package/src/internal/string.ts +0 -128
- package/src/internal/subscription.ts +0 -264
- package/src/internal/value/compare.ts +0 -170
- package/src/internal/value/equal.ts +0 -465
- package/src/internal/value/get.ts +0 -57
- package/src/internal/value/handlers.ts +0 -95
- package/src/internal/value/has.ts +0 -118
- package/src/internal/value/misc.ts +0 -125
- package/src/internal/value/partial.ts +0 -46
- package/src/internal/value/set.ts +0 -132
- package/src/is.ts +0 -182
- package/src/logger.ts +0 -230
- package/src/math.ts +0 -281
- package/src/models.ts +0 -286
- package/src/number.ts +0 -1
- package/src/promise/delay.ts +0 -62
- package/src/promise/helpers.ts +0 -96
- package/src/promise/index.ts +0 -300
- package/src/promise/misc.ts +0 -96
- package/src/promise/models.ts +0 -195
- package/src/promise/timed.ts +0 -91
- package/src/query.ts +0 -147
- package/src/queue.ts +0 -814
- package/src/random.ts +0 -125
- package/src/result/index.ts +0 -105
- package/src/result/match.ts +0 -124
- package/src/result/misc.ts +0 -129
- package/src/result/models.ts +0 -83
- package/src/result/work/flow.ts +0 -488
- package/src/result/work/pipe.ts +0 -496
- package/src/sized/map.ts +0 -110
- package/src/sized/set.ts +0 -107
- package/src/string/case.ts +0 -236
- package/src/string/fuzzy.ts +0 -444
- package/src/string/index.ts +0 -199
- package/src/string/match.ts +0 -80
- package/src/string/normalize.ts +0 -205
- package/src/string/template.ts +0 -161
- package/src/value/clone.ts +0 -319
- package/src/value/collection.ts +0 -63
- package/src/value/diff.ts +0 -232
- package/src/value/freeze.ts +0 -322
- package/src/value/handle.ts +0 -3
- package/src/value/index.ts +0 -5
- package/src/value/merge.ts +0 -278
- package/src/value/omit.ts +0 -20
- package/src/value/pick.ts +0 -20
- package/src/value/shake.ts +0 -45
- package/src/value/smush.ts +0 -89
- package/src/value/transform.ts +0 -153
- package/src/value/unsmush.ts +0 -84
package/src/string/case.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import {memoize, type Memoized} from '../function/memoize';
|
|
2
|
-
import {join, words} from '../internal/string';
|
|
3
|
-
|
|
4
|
-
// #region Types
|
|
5
|
-
|
|
6
|
-
type Case = 'camel' | 'kebab' | 'pascal' | 'snake';
|
|
7
|
-
|
|
8
|
-
type Options = {
|
|
9
|
-
capitalizeAny: boolean;
|
|
10
|
-
capitalizeFirst: boolean;
|
|
11
|
-
type: Case;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// #endregion
|
|
15
|
-
|
|
16
|
-
// #region Functions
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Convert a string to camel case _(thisIsCamelCase)_
|
|
20
|
-
*
|
|
21
|
-
* @param value String to convert
|
|
22
|
-
* @returns Camel-cased string
|
|
23
|
-
*/
|
|
24
|
-
export function camelCase(value: string): string {
|
|
25
|
-
return toCase(CASE_CAMEL, value, true, false);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Capitalize the first letter of a string _(and lowercase the rest)_
|
|
30
|
-
*
|
|
31
|
-
* @param value String to capitalize
|
|
32
|
-
* @returns Capitalized string
|
|
33
|
-
*/
|
|
34
|
-
export function capitalize(value: string): string {
|
|
35
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
36
|
-
return '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
memoizedCapitalize ??= memoize(v =>
|
|
40
|
-
v.length === 1
|
|
41
|
-
? v.toLocaleUpperCase()
|
|
42
|
-
: `${v.charAt(0).toLocaleUpperCase()}${v.slice(1).toLocaleLowerCase()}`,
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
return memoizedCapitalize.run(value);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Convert a string to kebab case _(this-is-kebab-case)_
|
|
50
|
-
*
|
|
51
|
-
* @param value String to convert
|
|
52
|
-
* @returns Kebab-cased string
|
|
53
|
-
*/
|
|
54
|
-
export function kebabCase(value: string): string {
|
|
55
|
-
return toCase(CASE_KEBAB, value, false, false);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Convert a string to lower case
|
|
60
|
-
*
|
|
61
|
-
* @param value String to convert
|
|
62
|
-
* @returns Lower-cased string
|
|
63
|
-
*/
|
|
64
|
-
export function lowerCase(value: string): string {
|
|
65
|
-
if (typeof value !== 'string') {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
memoizedLowerCase ??= memoize(v => v.toLocaleLowerCase());
|
|
70
|
-
|
|
71
|
-
return memoizedLowerCase.run(value);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Convert a string to pascal case _(ThisIsPascalCase)_
|
|
76
|
-
*
|
|
77
|
-
* @param value String to convert
|
|
78
|
-
* @returns Pascal-cased string
|
|
79
|
-
*/
|
|
80
|
-
export function pascalCase(value: string): string {
|
|
81
|
-
return toCase(CASE_PASCAL, value, true, true);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Convert a string to snake case _(this_is_snake_case)_
|
|
86
|
-
*
|
|
87
|
-
* @param value String to convert
|
|
88
|
-
* @returns Snake-cased string
|
|
89
|
-
*/
|
|
90
|
-
export function snakeCase(value: string): string {
|
|
91
|
-
return toCase(CASE_SNAKE, value, false, false);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Convert a string to title case _(Capitalizing Every Word)_
|
|
96
|
-
*
|
|
97
|
-
* @param value String to convert
|
|
98
|
-
* @returns Title-cased string
|
|
99
|
-
*/
|
|
100
|
-
export function titleCase(value: string): string {
|
|
101
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
102
|
-
return '';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
memoizedTitleCase ??= memoize(v =>
|
|
106
|
-
v.length < 2 ? capitalize(v) : join(words(v).map(capitalize), ' '),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
return memoizedTitleCase.run(value);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function toCase(
|
|
113
|
-
type: Case,
|
|
114
|
-
value: string,
|
|
115
|
-
capitalizeAny: boolean,
|
|
116
|
-
capitalizeFirst: boolean,
|
|
117
|
-
): string {
|
|
118
|
-
caseMemoizers[type] ??= memoize(toCaseCallback.bind({type, capitalizeAny, capitalizeFirst}));
|
|
119
|
-
|
|
120
|
-
return caseMemoizers[type].run(value);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function toCaseCallback(this: Options, value: string): string {
|
|
124
|
-
if (typeof value !== 'string') {
|
|
125
|
-
return '';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (value.length < 1) {
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const {capitalizeAny, capitalizeFirst, type} = this;
|
|
133
|
-
|
|
134
|
-
const parts = words(value);
|
|
135
|
-
const partsLength = parts.length;
|
|
136
|
-
|
|
137
|
-
const cased: string[] = [];
|
|
138
|
-
|
|
139
|
-
for (let partIndex = 0; partIndex < partsLength; partIndex += 1) {
|
|
140
|
-
const part = parts[partIndex];
|
|
141
|
-
|
|
142
|
-
const acronymParts = part.replace(EXPRESSION_ACRONYM, (full, one, two, three) =>
|
|
143
|
-
three === S ? full : `${one}-${two}${three}`,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const camelCaseParts = acronymParts.replace(EXPRESSION_CAMEL_CASE, REPLACEMENT_CAMEL_CASE);
|
|
147
|
-
|
|
148
|
-
const items = camelCaseParts.split('-');
|
|
149
|
-
const itemsLength = items.length;
|
|
150
|
-
|
|
151
|
-
const partResult: string[] = [];
|
|
152
|
-
|
|
153
|
-
let itemCount = 0;
|
|
154
|
-
|
|
155
|
-
for (let itemIndex = 0; itemIndex < itemsLength; itemIndex += 1) {
|
|
156
|
-
const item = items[itemIndex];
|
|
157
|
-
|
|
158
|
-
if (item.length === 0) {
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!capitalizeAny || (itemCount === 0 && partIndex === 0 && !capitalizeFirst)) {
|
|
163
|
-
partResult.push(item.toLocaleLowerCase());
|
|
164
|
-
} else {
|
|
165
|
-
partResult.push(capitalize(item));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
itemCount += 1;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
cased.push(join(partResult, delimiters[type]));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return join(cased, delimiters[type]);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Convert a string to upper case
|
|
179
|
-
*
|
|
180
|
-
* @param value String to convert
|
|
181
|
-
* @returns Upper-cased string
|
|
182
|
-
*/
|
|
183
|
-
export function upperCase(value: string): string {
|
|
184
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
185
|
-
return '';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
memoizedUpperCase ??= memoize(v => v.toLocaleUpperCase());
|
|
189
|
-
|
|
190
|
-
return memoizedUpperCase.run(value);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// #endregion
|
|
194
|
-
|
|
195
|
-
// #region Variables
|
|
196
|
-
|
|
197
|
-
const CASE_CAMEL: Case = 'camel';
|
|
198
|
-
|
|
199
|
-
const CASE_KEBAB: Case = 'kebab';
|
|
200
|
-
|
|
201
|
-
const CASE_PASCAL: Case = 'pascal';
|
|
202
|
-
|
|
203
|
-
const CASE_SNAKE: Case = 'snake';
|
|
204
|
-
|
|
205
|
-
const DELIMTER_EMPTY = '';
|
|
206
|
-
|
|
207
|
-
const DELIMITER_HYPHEN = '-';
|
|
208
|
-
|
|
209
|
-
const DELIMITER_UNDERSCORE = '_';
|
|
210
|
-
|
|
211
|
-
const EXPRESSION_CAMEL_CASE = /(\p{Ll})(\p{Lu})/gu;
|
|
212
|
-
|
|
213
|
-
const EXPRESSION_ACRONYM = /(\p{Lu}*)(\p{Lu})(\p{Ll}+)/gu;
|
|
214
|
-
|
|
215
|
-
const REPLACEMENT_CAMEL_CASE = '$1-$2';
|
|
216
|
-
|
|
217
|
-
const S = 's';
|
|
218
|
-
|
|
219
|
-
const caseMemoizers: Partial<Record<string, Memoized<typeof toCaseCallback>>> = {};
|
|
220
|
-
|
|
221
|
-
const delimiters: Record<Case, string> = {
|
|
222
|
-
[CASE_CAMEL]: DELIMTER_EMPTY,
|
|
223
|
-
[CASE_KEBAB]: DELIMITER_HYPHEN,
|
|
224
|
-
[CASE_PASCAL]: DELIMTER_EMPTY,
|
|
225
|
-
[CASE_SNAKE]: DELIMITER_UNDERSCORE,
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
let memoizedCapitalize: Memoized<(value: string) => string>;
|
|
229
|
-
|
|
230
|
-
let memoizedLowerCase: Memoized<(value: string) => string>;
|
|
231
|
-
|
|
232
|
-
let memoizedTitleCase: Memoized<(value: string) => string>;
|
|
233
|
-
|
|
234
|
-
let memoizedUpperCase: Memoized<(value: string) => string>;
|
|
235
|
-
|
|
236
|
-
// #endregion
|
package/src/string/fuzzy.ts
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
import {isPlainObject} from '../internal/is';
|
|
2
|
-
import {getString} from '../internal/string';
|
|
3
|
-
import type {PlainObject, RequiredKeys} from '../models';
|
|
4
|
-
import {lowerCase} from './case';
|
|
5
|
-
import {includes} from './match';
|
|
6
|
-
|
|
7
|
-
// #region Types
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Fuzzy searcher for an array of items
|
|
11
|
-
*/
|
|
12
|
-
export type Fuzzy<Item> = {
|
|
13
|
-
/**
|
|
14
|
-
* Get items currently being searched through
|
|
15
|
-
*
|
|
16
|
-
* @returns Original items
|
|
17
|
-
*/
|
|
18
|
-
get items(): Item[];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Set new items to search through
|
|
22
|
-
*
|
|
23
|
-
* @param items New items to search through
|
|
24
|
-
*/
|
|
25
|
-
set items(items: Item[]);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
29
|
-
*
|
|
30
|
-
* @returns Stringified items
|
|
31
|
-
*/
|
|
32
|
-
get strings(): string[];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Search for items matching the given value
|
|
36
|
-
*
|
|
37
|
-
* @param value Value to search for
|
|
38
|
-
* @param options Search options
|
|
39
|
-
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
40
|
-
*/
|
|
41
|
-
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Search for items matching the given value
|
|
45
|
-
*
|
|
46
|
-
* @param value Value to search for
|
|
47
|
-
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
48
|
-
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
49
|
-
*/
|
|
50
|
-
search(value: string, limit: number): FuzzyResult<Item>;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export type FuzzyConfiguration<Item> = {
|
|
54
|
-
/**
|
|
55
|
-
* Handler to stringify items
|
|
56
|
-
*
|
|
57
|
-
* - May be a function that takes an item and returns a string, or if items are plain objects, a key of the item to use to grab a string value from
|
|
58
|
-
* - Defaults to `getString`
|
|
59
|
-
*/
|
|
60
|
-
handler?: (item: Item) => string;
|
|
61
|
-
} & (Item extends PlainObject
|
|
62
|
-
? {
|
|
63
|
-
/**
|
|
64
|
-
* Key to use to stringify items
|
|
65
|
-
*
|
|
66
|
-
* _Prioritized over `handler`_
|
|
67
|
-
*/
|
|
68
|
-
key?: keyof Item;
|
|
69
|
-
}
|
|
70
|
-
: {}) &
|
|
71
|
-
FuzzyOptions;
|
|
72
|
-
|
|
73
|
-
type FuzzyItem<Item> = {
|
|
74
|
-
item: Item;
|
|
75
|
-
haystack: string;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export type FuzzyOptions = {
|
|
79
|
-
/**
|
|
80
|
-
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
81
|
-
*/
|
|
82
|
-
limit?: number;
|
|
83
|
-
/**
|
|
84
|
-
* Maximum score difference between the best and worst similar matches included in results
|
|
85
|
-
*
|
|
86
|
-
* - Higher values cast a wider net
|
|
87
|
-
* - Defaults to `5`
|
|
88
|
-
*/
|
|
89
|
-
tolerance?: number;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Search results from a fuzzy search, with exact and similar matches
|
|
94
|
-
*/
|
|
95
|
-
export type FuzzyResult<Item> = {
|
|
96
|
-
/**
|
|
97
|
-
* Ordered items that exactly match the search value
|
|
98
|
-
*/
|
|
99
|
-
exact: Item[];
|
|
100
|
-
/**
|
|
101
|
-
* Ordered items that are similar to the search value, ranked by relevance
|
|
102
|
-
*/
|
|
103
|
-
similar: Item[];
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Options for fuzzy searching
|
|
108
|
-
*/
|
|
109
|
-
export type FuzzySearchOptions = FuzzyOptions;
|
|
110
|
-
|
|
111
|
-
type FuzzyState<Item> = {
|
|
112
|
-
handler: (item: Item) => string;
|
|
113
|
-
items: Item[];
|
|
114
|
-
limit?: number;
|
|
115
|
-
strings: string[];
|
|
116
|
-
tolerance: number;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// #endregion
|
|
120
|
-
|
|
121
|
-
// #region Functions
|
|
122
|
-
|
|
123
|
-
function getHandler<Item>(input: unknown): (item: Item) => string {
|
|
124
|
-
if (input == null || input === getString) {
|
|
125
|
-
return getString;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
switch (typeof input) {
|
|
129
|
-
case 'function':
|
|
130
|
-
return input as (item: Item) => string;
|
|
131
|
-
|
|
132
|
-
case 'string':
|
|
133
|
-
return (item: Item) => (item as PlainObject)[input] as string;
|
|
134
|
-
|
|
135
|
-
default: {
|
|
136
|
-
if (isPlainObject(input)) {
|
|
137
|
-
return getHandler(
|
|
138
|
-
(input as FuzzyConfiguration<PlainObject>).key ??
|
|
139
|
-
(input as FuzzyConfiguration<Item>).handler,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
throw new TypeError(MESSAGE_HANDLER);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function getItems<Item>(items: FuzzyItem<Item>[]): Item[] {
|
|
149
|
-
return items
|
|
150
|
-
.sort((first, second) => first.haystack.localeCompare(second.haystack))
|
|
151
|
-
.map(({item}) => item);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getFuzzyOptions<Item>(
|
|
155
|
-
input: unknown,
|
|
156
|
-
state?: FuzzyState<Item>,
|
|
157
|
-
): RequiredKeys<FuzzyOptions, 'tolerance'> {
|
|
158
|
-
const options: FuzzyOptions = isPlainObject(input) ? input : {};
|
|
159
|
-
|
|
160
|
-
const limit = typeof input === 'number' ? input : options.limit;
|
|
161
|
-
|
|
162
|
-
if (typeof limit === 'number' && !Number.isNaN(limit) && limit >= 1) {
|
|
163
|
-
options.limit = Math.floor(limit);
|
|
164
|
-
} else {
|
|
165
|
-
options.limit = state?.limit;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
options.tolerance = getTolerance(options.tolerance, state);
|
|
169
|
-
|
|
170
|
-
return options as RequiredKeys<FuzzyOptions, 'tolerance'>;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function getFuzzyState<Item>(items: Item[], input: unknown): FuzzyState<Item> {
|
|
174
|
-
const handler = getHandler(input);
|
|
175
|
-
const options = getFuzzyOptions(input);
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
handler,
|
|
179
|
-
items: items.slice(),
|
|
180
|
-
limit: options.limit,
|
|
181
|
-
strings: items.map(handler),
|
|
182
|
-
tolerance: options.tolerance,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getTolerance<Item>(input: unknown, state?: FuzzyState<Item>): number {
|
|
187
|
-
if (typeof input === 'number' && !Number.isNaN(input)) {
|
|
188
|
-
return input < 0 ? 0 : Math.floor(input);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return state?.tolerance ?? PROXIMITY_THRESHOLD;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Create a fuzzy searcher for an array of items
|
|
196
|
-
*
|
|
197
|
-
* @param items Items to search through
|
|
198
|
-
* @param key Key to use to stringify items
|
|
199
|
-
* @returns Fuzzy searcher
|
|
200
|
-
*/
|
|
201
|
-
export function fuzzy<Item extends PlainObject, ItemKey extends keyof Item>(
|
|
202
|
-
items: Item[],
|
|
203
|
-
key?: ItemKey,
|
|
204
|
-
): Fuzzy<Item>;
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Create a fuzzy searcher for an array of items
|
|
208
|
-
*
|
|
209
|
-
* @param items Items to search through
|
|
210
|
-
* @param handler Handler to stringify items
|
|
211
|
-
* @returns Fuzzy searcher
|
|
212
|
-
*/
|
|
213
|
-
export function fuzzy<Item>(items: Item[], handler?: (item: Item) => string): Fuzzy<Item>;
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Create a fuzzy searcher for an array of items
|
|
217
|
-
*
|
|
218
|
-
* @param items Items to search through
|
|
219
|
-
* @param configuration Fuzzy configuration
|
|
220
|
-
* @returns Fuzzy searcher
|
|
221
|
-
*/
|
|
222
|
-
export function fuzzy<Item>(items: Item[], configuration?: FuzzyConfiguration<Item>): Fuzzy<Item>;
|
|
223
|
-
|
|
224
|
-
export function fuzzy(items: unknown[], configuration?: unknown): Fuzzy<unknown> {
|
|
225
|
-
if (!Array.isArray(items)) {
|
|
226
|
-
throw new TypeError(MESSAGE_ARRAY);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const state = getFuzzyState(items, configuration);
|
|
230
|
-
|
|
231
|
-
const instance = {
|
|
232
|
-
search: (value: string, options?: number | FuzzyOptions) =>
|
|
233
|
-
search(
|
|
234
|
-
state.items,
|
|
235
|
-
state.strings,
|
|
236
|
-
value,
|
|
237
|
-
options == null ? state : getFuzzyOptions(options, state),
|
|
238
|
-
),
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
Object.defineProperties(instance, {
|
|
242
|
-
items: {
|
|
243
|
-
get: () => state.items.slice(),
|
|
244
|
-
set: (items: unknown[]) => {
|
|
245
|
-
if (!Array.isArray(items)) {
|
|
246
|
-
throw new TypeError(MESSAGE_ARRAY);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
state.items = items.slice();
|
|
250
|
-
state.strings = items.map(state.handler);
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
strings: {
|
|
254
|
-
get: () => state.strings.slice(),
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return Object.freeze(instance) as Fuzzy<unknown>;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
fuzzy.match = fuzzyMatch;
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Does the needle match the haystack in a fuzzy way?
|
|
265
|
-
*
|
|
266
|
-
* @param haystack Haystack to search through
|
|
267
|
-
* @param needle Needle to search for
|
|
268
|
-
* @returns `true` if the needle matches the haystack in a fuzzy way, otherwise `false`
|
|
269
|
-
*/
|
|
270
|
-
export function fuzzyMatch(haystack: string, needle: string): boolean {
|
|
271
|
-
if (typeof haystack !== 'string' || typeof needle !== 'string') {
|
|
272
|
-
return false;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const trimmed = needle.trim();
|
|
276
|
-
|
|
277
|
-
if (includes(haystack, trimmed, true)) {
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return getScore(haystack, trimmed) > -1;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function isSubsequence(haystack: string, needle: string): boolean {
|
|
285
|
-
const lowerCaseHaystack = lowerCase(haystack);
|
|
286
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
287
|
-
|
|
288
|
-
const haystackLength = lowerCaseHaystack.length;
|
|
289
|
-
const needleLength = lowerCaseNeedle.length;
|
|
290
|
-
|
|
291
|
-
let needleIndex = 0;
|
|
292
|
-
|
|
293
|
-
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
294
|
-
// Advance needle pointer only on a matching character
|
|
295
|
-
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
296
|
-
needleIndex += 1;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// All needle characters matched in order
|
|
300
|
-
if (needleIndex === needleLength) {
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function getScore(haystack: string, needle: string): number {
|
|
309
|
-
if (!isSubsequence(haystack, needle)) {
|
|
310
|
-
return -1;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const lowerCaseHaystack = lowerCase(haystack);
|
|
314
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
315
|
-
|
|
316
|
-
const needleLength = lowerCaseNeedle.length;
|
|
317
|
-
|
|
318
|
-
let needleIndex = 0;
|
|
319
|
-
let previousMatchIndex = -1;
|
|
320
|
-
let score = 0;
|
|
321
|
-
|
|
322
|
-
for (let haystackIndex = 0; haystackIndex < lowerCaseHaystack.length; haystackIndex += 1) {
|
|
323
|
-
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
324
|
-
// +1 for each matched character
|
|
325
|
-
score += 1;
|
|
326
|
-
|
|
327
|
-
// Bonus for matching at the start of the string
|
|
328
|
-
if (haystackIndex === 0) {
|
|
329
|
-
score += 1;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Proximity bonus: decays as gap between consecutive matches widens
|
|
333
|
-
if (previousMatchIndex !== -1) {
|
|
334
|
-
const gap = haystackIndex - previousMatchIndex - 1;
|
|
335
|
-
|
|
336
|
-
score += Math.max(0, PROXIMITY_THRESHOLD - gap);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
previousMatchIndex = haystackIndex;
|
|
340
|
-
|
|
341
|
-
needleIndex += 1;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// All needle characters matched; no need to scan further
|
|
345
|
-
if (needleIndex === needleLength) {
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Penalty for longer strings to favour tighter matches
|
|
351
|
-
score -= Math.floor(lowerCaseHaystack.length / LENGTH_DIVISOR);
|
|
352
|
-
|
|
353
|
-
return Math.max(0, score);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function search<Item>(
|
|
357
|
-
items: Item[],
|
|
358
|
-
strings: string[],
|
|
359
|
-
input: string,
|
|
360
|
-
options: RequiredKeys<FuzzyOptions, 'tolerance'>,
|
|
361
|
-
) {
|
|
362
|
-
const result: FuzzyResult<Item> = {
|
|
363
|
-
exact: [],
|
|
364
|
-
similar: [],
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const value = typeof input === 'string' ? input.trim() : '';
|
|
368
|
-
|
|
369
|
-
if (value.length === 0) {
|
|
370
|
-
result.exact = items.slice(0, options.limit);
|
|
371
|
-
|
|
372
|
-
return result;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
let {length} = items;
|
|
376
|
-
|
|
377
|
-
const exact: Array<FuzzyItem<Item>> = [];
|
|
378
|
-
const similar: Array<Item> = [];
|
|
379
|
-
|
|
380
|
-
const scored: Record<number, Array<FuzzyItem<Item>>> = {};
|
|
381
|
-
|
|
382
|
-
for (let index = 0; index < length; index += 1) {
|
|
383
|
-
const item = items[index];
|
|
384
|
-
const haystack = strings[index];
|
|
385
|
-
|
|
386
|
-
if (includes(haystack, value, true)) {
|
|
387
|
-
exact.push({item, haystack});
|
|
388
|
-
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const score = getScore(haystack, value);
|
|
393
|
-
|
|
394
|
-
if (score > -1) {
|
|
395
|
-
scored[score] ??= [];
|
|
396
|
-
|
|
397
|
-
scored[score].push({item, haystack});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const keys = Object.keys(scored)
|
|
402
|
-
.map(Number)
|
|
403
|
-
.sort((first, second) => second - first);
|
|
404
|
-
|
|
405
|
-
length = keys.length;
|
|
406
|
-
|
|
407
|
-
if (length > 0 && options.tolerance > 0) {
|
|
408
|
-
const maxScore = keys[0];
|
|
409
|
-
|
|
410
|
-
for (let index = 0; index < length; index += 1) {
|
|
411
|
-
const key = keys[index];
|
|
412
|
-
|
|
413
|
-
if (maxScore - key > options.tolerance) {
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
similar.push(...getItems(scored[key]));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
result.exact = getItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
422
|
-
|
|
423
|
-
if (options.limit == null) {
|
|
424
|
-
result.similar = similar;
|
|
425
|
-
} else {
|
|
426
|
-
result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return result;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// #endregion
|
|
433
|
-
|
|
434
|
-
// #region Variables
|
|
435
|
-
|
|
436
|
-
const LENGTH_DIVISOR = 3;
|
|
437
|
-
|
|
438
|
-
const MESSAGE_ARRAY = 'Fuzzy requires an array of items';
|
|
439
|
-
|
|
440
|
-
const MESSAGE_HANDLER = 'Fuzzy requires a key or function to stringify items';
|
|
441
|
-
|
|
442
|
-
const PROXIMITY_THRESHOLD = 5;
|
|
443
|
-
|
|
444
|
-
// #endregion
|