@oscarpalmer/atoms 0.219.0 → 0.221.0
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 +57 -19
- package/dist/index.mjs +285 -203
- package/dist/internal/function/assert.mjs +1 -3
- package/dist/internal/string/case.d.mts +2 -0
- package/dist/internal/string/case.mjs +36 -29
- package/dist/internal/subscription.mjs +2 -6
- package/dist/models/string/string.case.model.d.mts +8 -3
- package/dist/models/string/string.case.model.mjs +9 -4
- package/dist/models/string/string.fuzzy.model.d.mts +46 -18
- package/dist/models/string/string.fuzzy.model.mjs +3 -1
- package/dist/models/string/string.misc.model.mjs +1 -1
- package/dist/models/string/string.template.model.mjs +1 -1
- package/dist/string/fuzzy.d.mts +3 -3
- package/dist/string/fuzzy.mjs +171 -97
- package/package.json +1 -1
package/dist/string/fuzzy.mjs
CHANGED
|
@@ -3,8 +3,7 @@ import { floor } from "../internal/math/misc.mjs";
|
|
|
3
3
|
import { max } from "../internal/math/aggregate.mjs";
|
|
4
4
|
import { getString } from "../internal/string/misc.mjs";
|
|
5
5
|
import { lowerCase } from "../internal/string/case.mjs";
|
|
6
|
-
import { FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_SYMBOL } from "../models/string/string.fuzzy.model.mjs";
|
|
7
|
-
import { includes } from "./match.mjs";
|
|
6
|
+
import { FUZZY_BOUNDARY_SEPARATOR, FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_SYMBOL } from "../models/string/string.fuzzy.model.mjs";
|
|
8
7
|
//#region src/string/fuzzy.ts
|
|
9
8
|
function Fuzzy(state) {
|
|
10
9
|
this[FUZZY_SYMBOL] = state;
|
|
@@ -14,39 +13,76 @@ Fuzzy.prototype.search = search;
|
|
|
14
13
|
Object.defineProperties(Fuzzy.prototype, {
|
|
15
14
|
items: {
|
|
16
15
|
enumerable: true,
|
|
17
|
-
get:
|
|
18
|
-
set:
|
|
16
|
+
get: getItems,
|
|
17
|
+
set: setItems
|
|
19
18
|
},
|
|
20
19
|
strings: {
|
|
21
20
|
enumerable: true,
|
|
22
|
-
get:
|
|
21
|
+
get: getStrings
|
|
23
22
|
}
|
|
24
23
|
});
|
|
25
|
-
function
|
|
26
|
-
return
|
|
24
|
+
function compareItem(sorter) {
|
|
25
|
+
return (first, second) => sorter(first.string.original, second.string.original);
|
|
27
26
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
function compareRanked(sorter) {
|
|
28
|
+
return (first, second) => first.rank - second.rank || first.position - second.position || sorter(first.string.original, second.string.original);
|
|
29
|
+
}
|
|
30
|
+
function createItems(state, items) {
|
|
31
|
+
state.items.original = items.map((item) => ({
|
|
32
|
+
data: item,
|
|
33
|
+
string: createString(state.handler(item))
|
|
34
|
+
}));
|
|
35
|
+
state.items.sorted = void 0;
|
|
36
|
+
}
|
|
37
|
+
function createOptions(input, state) {
|
|
38
|
+
const options = {};
|
|
39
|
+
const original = isPlainObject(input) ? input : {};
|
|
40
|
+
const limit = typeof input === "number" ? input : original.limit;
|
|
31
41
|
if (typeof limit === "number" && !Number.isNaN(limit) && limit >= 1) options.limit = floor(limit);
|
|
32
|
-
else options.limit = state?.limit;
|
|
33
|
-
options.tolerance = getTolerance(
|
|
42
|
+
else options.limit = state?.limit ?? Number.POSITIVE_INFINITY;
|
|
43
|
+
options.tolerance = getTolerance(original.tolerance, state);
|
|
34
44
|
return options;
|
|
35
45
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
function createRanked(item, needle, position) {
|
|
47
|
+
let boundary = position;
|
|
48
|
+
while (boundary > -1 && !isBoundary(item.string, boundary)) boundary = item.string.lowercase.indexOf(needle, boundary + 1);
|
|
49
|
+
if (boundary === -1) return {
|
|
50
|
+
...item,
|
|
51
|
+
position,
|
|
52
|
+
rank: 2
|
|
53
|
+
};
|
|
39
54
|
return {
|
|
55
|
+
...item,
|
|
56
|
+
position: boundary,
|
|
57
|
+
rank: boundary === 0 ? 0 : 1
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createState(items, input) {
|
|
61
|
+
const handler = getHandler(input);
|
|
62
|
+
const options = createOptions(input);
|
|
63
|
+
const sorter = getComparator(input);
|
|
64
|
+
const state = {
|
|
65
|
+
compare: {
|
|
66
|
+
item: compareItem(sorter),
|
|
67
|
+
ranked: compareRanked(sorter)
|
|
68
|
+
},
|
|
40
69
|
handler,
|
|
41
|
-
items:
|
|
70
|
+
items: { original: [] },
|
|
42
71
|
limit: options.limit,
|
|
43
|
-
strings: items.map(handler),
|
|
44
72
|
tolerance: options.tolerance
|
|
45
73
|
};
|
|
74
|
+
createItems(state, items);
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
function createString(original) {
|
|
78
|
+
return {
|
|
79
|
+
original,
|
|
80
|
+
lowercase: lowerCase(original)
|
|
81
|
+
};
|
|
46
82
|
}
|
|
47
83
|
function fuzzy(items, configuration) {
|
|
48
84
|
if (!Array.isArray(items)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
49
|
-
return new Fuzzy(
|
|
85
|
+
return new Fuzzy(createState(items, configuration));
|
|
50
86
|
}
|
|
51
87
|
/**
|
|
52
88
|
* Does the needle match the haystack in a fuzzy way?
|
|
@@ -60,53 +96,117 @@ function fuzzy(items, configuration) {
|
|
|
60
96
|
function fuzzyMatch(haystack, needle) {
|
|
61
97
|
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
62
98
|
const trimmed = needle.trim();
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
const lowercaseHaystack = lowerCase(haystack);
|
|
100
|
+
const lowercaseNeedle = lowerCase(trimmed);
|
|
101
|
+
if (lowercaseHaystack.includes(lowercaseNeedle)) return true;
|
|
102
|
+
return getScore({
|
|
103
|
+
lowercase: lowercaseHaystack,
|
|
104
|
+
original: haystack
|
|
105
|
+
}, lowercaseNeedle) > -1;
|
|
106
|
+
}
|
|
107
|
+
function getComparator(input) {
|
|
108
|
+
if (isPlainObject(input) && typeof input.compare === "function") return input.compare;
|
|
109
|
+
defaultSorter ??= Intl.Collator().compare;
|
|
110
|
+
return defaultSorter;
|
|
65
111
|
}
|
|
66
112
|
function getHandler(input) {
|
|
67
113
|
if (input == null || input === getString) return getString;
|
|
68
114
|
switch (typeof input) {
|
|
69
|
-
case "function": return input;
|
|
70
|
-
case "
|
|
115
|
+
case "function": return (item) => getString(input(item));
|
|
116
|
+
case "number":
|
|
117
|
+
case "string":
|
|
118
|
+
case "symbol": return (item) => getString(item?.[input]);
|
|
71
119
|
default:
|
|
72
120
|
if (isPlainObject(input)) return getHandler(input.key ?? input.handler);
|
|
73
121
|
throw new TypeError(FUZZY_MESSAGE_HANDLER);
|
|
74
122
|
}
|
|
75
123
|
}
|
|
76
|
-
function
|
|
77
|
-
return this[FUZZY_SYMBOL].items.
|
|
78
|
-
}
|
|
79
|
-
function getFuzzyStrings() {
|
|
80
|
-
return this[FUZZY_SYMBOL].strings.slice();
|
|
124
|
+
function getItems() {
|
|
125
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.data);
|
|
81
126
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
86
|
-
const needleLength = lowerCaseNeedle.length;
|
|
127
|
+
function getMatchEnd(haystack, needle, cursor) {
|
|
128
|
+
const haystackLength = haystack.length;
|
|
129
|
+
const needleLength = needle.length;
|
|
87
130
|
let needleIndex = 0;
|
|
88
|
-
let
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
for (let index = cursor; index < haystackLength; index += 1) if (haystack[index] === needle[needleIndex]) {
|
|
132
|
+
needleIndex += 1;
|
|
133
|
+
if (needleIndex === needleLength) return index;
|
|
134
|
+
}
|
|
135
|
+
return -1;
|
|
136
|
+
}
|
|
137
|
+
function getResult(state, exact, included, scored, options) {
|
|
138
|
+
const result = {
|
|
139
|
+
exact: [],
|
|
140
|
+
included: [],
|
|
141
|
+
similar: []
|
|
142
|
+
};
|
|
143
|
+
exact.sort(state.compare.item);
|
|
144
|
+
result.exact = exact.slice(0, options.limit).map((item) => item.data);
|
|
145
|
+
included.sort(state.compare.ranked);
|
|
146
|
+
result.included = included.slice(0, options.limit - result.exact.length).map((item) => item.data);
|
|
147
|
+
const remaining = options.limit - result.exact.length - result.included.length;
|
|
148
|
+
if (remaining === 0) return result;
|
|
149
|
+
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
150
|
+
const keysLength = keys.length;
|
|
151
|
+
if (keysLength === 0) return result;
|
|
152
|
+
const maxScore = keys[0];
|
|
153
|
+
outer: for (let index = 0; index < keysLength; index += 1) {
|
|
154
|
+
const key = keys[index];
|
|
155
|
+
if (maxScore - key > options.tolerance) break;
|
|
156
|
+
const keyed = scored[key];
|
|
157
|
+
const keyedLength = keyed.length;
|
|
158
|
+
keyed.sort(state.compare.item);
|
|
159
|
+
for (let keyedIndex = 0; keyedIndex < keyedLength; keyedIndex += 1) {
|
|
160
|
+
result.similar.push(keyed[keyedIndex].data);
|
|
161
|
+
if (result.similar.length >= remaining) break outer;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function getScore(item, needle) {
|
|
167
|
+
const haystackLength = item.lowercase.length;
|
|
168
|
+
const needleLength = needle.length;
|
|
169
|
+
if (needleLength > haystackLength) return -1;
|
|
170
|
+
let best = -1;
|
|
171
|
+
let cursor = 0;
|
|
172
|
+
while (cursor < haystackLength) {
|
|
173
|
+
const end = getMatchEnd(item.lowercase, needle, cursor);
|
|
174
|
+
if (end === -1) break;
|
|
175
|
+
let index = end;
|
|
176
|
+
let needleIndex = needleLength - 1;
|
|
177
|
+
let next = -1;
|
|
178
|
+
let score = 0;
|
|
179
|
+
let start = end;
|
|
180
|
+
while (needleIndex >= 0) {
|
|
181
|
+
if (item.lowercase[index] === needle[needleIndex]) {
|
|
182
|
+
score += 1;
|
|
183
|
+
if (next !== -1) score += max([0, 5 - (next - index - 1)]);
|
|
184
|
+
if (isBoundary(item, index)) score += 2;
|
|
185
|
+
next = index;
|
|
186
|
+
start = index;
|
|
187
|
+
needleIndex -= 1;
|
|
97
188
|
}
|
|
98
|
-
|
|
99
|
-
needleIndex += 1;
|
|
189
|
+
index -= 1;
|
|
100
190
|
}
|
|
101
|
-
if (
|
|
191
|
+
if (start === 0) score += 1;
|
|
192
|
+
if (score > best) best = score;
|
|
193
|
+
cursor = start + 1;
|
|
102
194
|
}
|
|
103
|
-
|
|
104
|
-
return max([0,
|
|
195
|
+
if (best === -1) return -1;
|
|
196
|
+
return max([0, best - floor(haystackLength / 3)]);
|
|
197
|
+
}
|
|
198
|
+
function getStrings() {
|
|
199
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.string.original);
|
|
105
200
|
}
|
|
106
201
|
function getTolerance(input, state) {
|
|
107
202
|
if (typeof input === "number" && !Number.isNaN(input)) return input < 0 ? 0 : floor(input);
|
|
108
203
|
return state?.tolerance ?? 5;
|
|
109
204
|
}
|
|
205
|
+
function isBoundary(string, index) {
|
|
206
|
+
if (index === 0 || FUZZY_BOUNDARY_SEPARATOR.test(string.lowercase[index - 1])) return true;
|
|
207
|
+
if (string.original.length !== string.lowercase.length) return false;
|
|
208
|
+
return string.original[index] !== string.lowercase[index] && string.original[index - 1] === string.lowercase[index - 1];
|
|
209
|
+
}
|
|
110
210
|
/**
|
|
111
211
|
* Is the value a fuzzy searcher?
|
|
112
212
|
*
|
|
@@ -118,74 +218,48 @@ function getTolerance(input, state) {
|
|
|
118
218
|
function isFuzzy(value) {
|
|
119
219
|
return typeof value === "object" && value !== null && "$fuzzy" in value && value["$fuzzy"] === true;
|
|
120
220
|
}
|
|
121
|
-
function isSubsequence(haystack, needle) {
|
|
122
|
-
const lowerCaseHaystack = lowerCase(haystack);
|
|
123
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
124
|
-
const haystackLength = lowerCaseHaystack.length;
|
|
125
|
-
const needleLength = lowerCaseNeedle.length;
|
|
126
|
-
let needleIndex = 0;
|
|
127
|
-
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
128
|
-
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) needleIndex += 1;
|
|
129
|
-
if (needleIndex === needleLength) return true;
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
221
|
function search(input, option) {
|
|
134
222
|
const state = this[FUZZY_SYMBOL];
|
|
135
|
-
const options = option == null ? state :
|
|
136
|
-
const result = {
|
|
137
|
-
exact: [],
|
|
138
|
-
similar: []
|
|
139
|
-
};
|
|
223
|
+
const options = option == null ? state : createOptions(option, state);
|
|
140
224
|
const value = typeof input === "string" ? input.trim() : "";
|
|
225
|
+
const lowercaseValue = lowerCase(value);
|
|
141
226
|
if (value.length === 0) {
|
|
142
|
-
|
|
143
|
-
return
|
|
227
|
+
state.items.sorted ??= state.items.original.toSorted(state.compare.item);
|
|
228
|
+
return {
|
|
229
|
+
exact: state.items.sorted.slice(0, options.limit).map((item) => item.data),
|
|
230
|
+
included: [],
|
|
231
|
+
similar: []
|
|
232
|
+
};
|
|
144
233
|
}
|
|
145
|
-
let { length } = state.items;
|
|
146
234
|
const exact = [];
|
|
147
|
-
const
|
|
235
|
+
const included = [];
|
|
148
236
|
const scored = {};
|
|
237
|
+
const { length } = state.items.original;
|
|
149
238
|
for (let index = 0; index < length; index += 1) {
|
|
150
|
-
const item = state.items[index];
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
exact.push(
|
|
154
|
-
item,
|
|
155
|
-
haystack
|
|
156
|
-
});
|
|
239
|
+
const item = state.items.original[index];
|
|
240
|
+
const { string } = item;
|
|
241
|
+
if (string.lowercase === lowercaseValue) {
|
|
242
|
+
exact.push(item);
|
|
157
243
|
continue;
|
|
158
244
|
}
|
|
159
|
-
const
|
|
245
|
+
const position = string.lowercase.indexOf(lowercaseValue);
|
|
246
|
+
if (position > -1) {
|
|
247
|
+
included.push(createRanked(item, lowercaseValue, position));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const score = getScore(string, lowercaseValue);
|
|
160
251
|
if (score > -1) {
|
|
161
252
|
scored[score] ??= [];
|
|
162
|
-
scored[score].push(
|
|
163
|
-
item,
|
|
164
|
-
haystack
|
|
165
|
-
});
|
|
253
|
+
scored[score].push(item);
|
|
166
254
|
}
|
|
167
255
|
}
|
|
168
|
-
|
|
169
|
-
length = keys.length;
|
|
170
|
-
if (length > 0 && options.tolerance > 0) {
|
|
171
|
-
const maxScore = keys[0];
|
|
172
|
-
for (let index = 0; index < length; index += 1) {
|
|
173
|
-
const key = keys[index];
|
|
174
|
-
if (maxScore - key > options.tolerance) break;
|
|
175
|
-
similar.push(...createFuzzyItems(scored[key]));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
result.exact = createFuzzyItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
179
|
-
if (options.limit == null) result.similar = similar;
|
|
180
|
-
else result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
181
|
-
return result;
|
|
256
|
+
return getResult(state, exact, included, scored, options);
|
|
182
257
|
}
|
|
183
|
-
function
|
|
258
|
+
function setItems(value) {
|
|
184
259
|
if (!Array.isArray(value)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
185
|
-
|
|
186
|
-
state.items = value.slice();
|
|
187
|
-
state.strings = value.map((item) => state.handler(item));
|
|
260
|
+
createItems(this[FUZZY_SYMBOL], value);
|
|
188
261
|
}
|
|
262
|
+
let defaultSorter;
|
|
189
263
|
fuzzy.is = isFuzzy;
|
|
190
264
|
fuzzy.match = fuzzyMatch;
|
|
191
265
|
//#endregion
|