@mirta/basics 0.3.4 → 0.4.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/array/index.d.mts +61 -0
- package/dist/array/index.mjs +67 -0
- package/dist/fuzzy/index.d.mts +170 -0
- package/dist/fuzzy/index.mjs +557 -0
- package/dist/guards.mjs +133 -0
- package/dist/index.d.mts +236 -0
- package/dist/index.mjs +4 -9
- package/dist/object/index.d.mts +161 -0
- package/dist/object/index.mjs +56 -0
- package/package.json +28 -5
- package/dist/index.d.ts +0 -119
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Порог длины входной строки.
|
|
3
|
+
*
|
|
4
|
+
* Строки длиннее этого значения автоматически отклоняются,
|
|
5
|
+
* чтобы не допустить перегрузку памяти.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.4.0
|
|
8
|
+
*
|
|
9
|
+
**/
|
|
10
|
+
const INPUT_LENGTH_THRESHOLD = 500;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Нормализует абсолютное расстояние редактирования в относительные метрики.
|
|
14
|
+
*
|
|
15
|
+
* @param steps - Количество шагов
|
|
16
|
+
* @param length - Длина строки
|
|
17
|
+
* @returns Относительное расстояние
|
|
18
|
+
*
|
|
19
|
+
* @since 0.4.0
|
|
20
|
+
*
|
|
21
|
+
**/
|
|
22
|
+
function normalizeDistance(steps, length) {
|
|
23
|
+
const relative = length === 0 ? 0 : steps / length;
|
|
24
|
+
const similarity = 1 - relative;
|
|
25
|
+
return { steps, relative, similarity };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Создаёт матрицу размером `(sizeX + 1) × (sizeY + 1)`.
|
|
29
|
+
*
|
|
30
|
+
* @param sizeX - Размер по оси X
|
|
31
|
+
* @param sizeY - Размер по оси Y
|
|
32
|
+
* @param defaultValue - Значение по умолчанию
|
|
33
|
+
* @returns Матрица
|
|
34
|
+
*
|
|
35
|
+
* @since 0.4.0
|
|
36
|
+
*
|
|
37
|
+
**/
|
|
38
|
+
function createMatrix(sizeX, sizeY, defaultValue) {
|
|
39
|
+
const matrix = new Array(sizeX + 1);
|
|
40
|
+
for (let i = 0; i <= sizeX; i++) {
|
|
41
|
+
const row = new Array(sizeY + 1);
|
|
42
|
+
for (let j = 0; j <= sizeY; j++) {
|
|
43
|
+
row[j] = defaultValue;
|
|
44
|
+
}
|
|
45
|
+
matrix[i] = row;
|
|
46
|
+
}
|
|
47
|
+
return matrix;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Вычисляет расстояние Дамерау-Левенштейна между двумя строками.
|
|
51
|
+
*
|
|
52
|
+
* Сложность: `O(n × m)` 💥 Экспоненциальный рост.
|
|
53
|
+
*
|
|
54
|
+
* @param from Первая строка
|
|
55
|
+
* @param to Вторая строка
|
|
56
|
+
* @param maxDistance Максимальное расстояние для расчёта.
|
|
57
|
+
* Если превышено, расстояние возвращается как `Infinity`.
|
|
58
|
+
*
|
|
59
|
+
* @since 0.4.0
|
|
60
|
+
*
|
|
61
|
+
**/
|
|
62
|
+
function damerauLevenshtein(from, to, maxDistance) {
|
|
63
|
+
const lenFrom = from.length;
|
|
64
|
+
const lenTo = to.length;
|
|
65
|
+
// Предотвращает перегрузку памяти, если строки слишком длинные.
|
|
66
|
+
if (lenFrom > INPUT_LENGTH_THRESHOLD || lenTo > INPUT_LENGTH_THRESHOLD)
|
|
67
|
+
return { steps: Infinity, relative: Infinity, similarity: -Infinity };
|
|
68
|
+
const maxLength = Math.max(lenFrom, lenTo);
|
|
69
|
+
// Эффективный лимит: если maxDistance не задан, используем maxLength + 1
|
|
70
|
+
const effectiveLimit = maxDistance ?? maxLength + 1;
|
|
71
|
+
// Ранний выход: если разница в длинах больше limit
|
|
72
|
+
if (Math.abs(lenFrom - lenTo) > effectiveLimit)
|
|
73
|
+
return { steps: Infinity, relative: Infinity, similarity: -Infinity };
|
|
74
|
+
if (lenFrom === 0)
|
|
75
|
+
return lenTo > effectiveLimit
|
|
76
|
+
? { steps: Infinity, relative: Infinity, similarity: -Infinity }
|
|
77
|
+
: normalizeDistance(lenTo, maxLength);
|
|
78
|
+
if (lenTo === 0)
|
|
79
|
+
return lenFrom > effectiveLimit
|
|
80
|
+
? { steps: Infinity, relative: Infinity, similarity: -Infinity }
|
|
81
|
+
: normalizeDistance(lenFrom, maxLength);
|
|
82
|
+
// Создаём и заполняем матрицу размером (lenFrom + 1) × (lenTo + 1)
|
|
83
|
+
const matrix = createMatrix(lenFrom, lenTo, Infinity);
|
|
84
|
+
for (let i = 0; i <= lenFrom; i++)
|
|
85
|
+
matrix[i][0] = i;
|
|
86
|
+
for (let j = 0; j <= lenTo; j++)
|
|
87
|
+
matrix[0][j] = j;
|
|
88
|
+
for (let i = 1; i <= lenFrom; i++) {
|
|
89
|
+
// Оптимизация: рассматриваем только ячейки в пределах возможного расстояния
|
|
90
|
+
const jStart = Math.max(1, i - effectiveLimit);
|
|
91
|
+
const jEnd = Math.min(lenTo, i + effectiveLimit);
|
|
92
|
+
for (let j = jStart; j <= jEnd; j++) {
|
|
93
|
+
const cost = from[i - 1] === to[j - 1] ? 0 : 1;
|
|
94
|
+
let min = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
95
|
+
// Транспозиция: перестановка двух соседних символов
|
|
96
|
+
if (i > 1
|
|
97
|
+
&& j > 1
|
|
98
|
+
&& from[i - 1] === to[j - 2]
|
|
99
|
+
&& from[i - 2] === to[j - 1]) {
|
|
100
|
+
const transposition = matrix[i - 2][j - 2] + 1;
|
|
101
|
+
if (transposition < min)
|
|
102
|
+
min = transposition;
|
|
103
|
+
}
|
|
104
|
+
matrix[i][j] = min;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const steps = matrix[lenFrom][lenTo];
|
|
108
|
+
// Если задан maxDistance и фактическое расстояние превышает его,
|
|
109
|
+
// считаем строки "слишком далёкими" и возвращаем Infinity.
|
|
110
|
+
//
|
|
111
|
+
if (maxDistance !== undefined && steps > maxDistance)
|
|
112
|
+
return { steps: Infinity, relative: Infinity, similarity: -Infinity };
|
|
113
|
+
return normalizeDistance(steps, maxLength);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Дерево правил Daitch-Mokotoff.
|
|
118
|
+
* Каждое значение — массив `[начало, перед гласной, иначе]`
|
|
119
|
+
*
|
|
120
|
+
* @since 0.4.0
|
|
121
|
+
*
|
|
122
|
+
**/
|
|
123
|
+
const codes = Object.freeze({
|
|
124
|
+
A: {
|
|
125
|
+
0: [0, -1, -1],
|
|
126
|
+
I: [[0, 1, -1]],
|
|
127
|
+
J: [[0, 1, -1]],
|
|
128
|
+
Y: [[0, 1, -1]],
|
|
129
|
+
U: [[0, 7, -1]],
|
|
130
|
+
},
|
|
131
|
+
B: [[7, 7, 7]],
|
|
132
|
+
C: {
|
|
133
|
+
0: [5, 5, 5], // латиница
|
|
134
|
+
1: [4, 4, 4], // кириллица
|
|
135
|
+
Z: { 0: [4, 4, 4], S: [[4, 4, 4]] },
|
|
136
|
+
S: { 0: [4, 4, 4], Z: [[4, 4, 4]] },
|
|
137
|
+
K: [
|
|
138
|
+
[5, 5, 5], // латиница
|
|
139
|
+
[45, 45, 45], // кириллица
|
|
140
|
+
],
|
|
141
|
+
H: {
|
|
142
|
+
0: [5, 5, 5],
|
|
143
|
+
1: [4, 4, 4],
|
|
144
|
+
S: [[5, 54, 54]],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
D: {
|
|
148
|
+
0: [3, 3, 3],
|
|
149
|
+
T: [[3, 3, 3]],
|
|
150
|
+
Z: { 0: [4, 4, 4], H: [[4, 4, 4]], S: [[4, 4, 4]] },
|
|
151
|
+
S: { 0: [4, 4, 4], H: [[4, 4, 4]], Z: [[4, 4, 4]] },
|
|
152
|
+
R: { S: [[4, 4, 4]], Z: [[4, 4, 4]] },
|
|
153
|
+
},
|
|
154
|
+
E: {
|
|
155
|
+
0: [0, -1, -1],
|
|
156
|
+
I: [[0, 1, -1]],
|
|
157
|
+
J: [[0, 1, -1]],
|
|
158
|
+
Y: [[0, 1, -1]],
|
|
159
|
+
U: [[1, 1, -1]],
|
|
160
|
+
W: [[1, 1, -1]],
|
|
161
|
+
},
|
|
162
|
+
F: {
|
|
163
|
+
0: [7, 7, 7],
|
|
164
|
+
B: [[7, 7, 7]],
|
|
165
|
+
},
|
|
166
|
+
G: [[5, 5, 5]],
|
|
167
|
+
H: [[5, 5, -1]],
|
|
168
|
+
I: {
|
|
169
|
+
0: [0, -1, -1],
|
|
170
|
+
A: [[1, -1, -1]],
|
|
171
|
+
E: [[1, -1, -1]],
|
|
172
|
+
O: [[1, -1, -1]],
|
|
173
|
+
U: [[1, -1, -1]],
|
|
174
|
+
},
|
|
175
|
+
J: [[4, 4, 4]],
|
|
176
|
+
K: {
|
|
177
|
+
0: [5, 5, 5],
|
|
178
|
+
H: [[5, 5, 5]],
|
|
179
|
+
S: [[5, 54, 54]],
|
|
180
|
+
},
|
|
181
|
+
L: [[8, 8, 8]],
|
|
182
|
+
M: {
|
|
183
|
+
0: [6, 6, 6],
|
|
184
|
+
N: [[66, 66, 66]],
|
|
185
|
+
},
|
|
186
|
+
N: {
|
|
187
|
+
0: [6, 6, 6],
|
|
188
|
+
M: [[66, 66, 66]],
|
|
189
|
+
},
|
|
190
|
+
O: {
|
|
191
|
+
0: [0, -1, -1],
|
|
192
|
+
I: [[0, 1, -1]],
|
|
193
|
+
J: [[0, 1, -1]],
|
|
194
|
+
Y: [[0, 1, -1]],
|
|
195
|
+
},
|
|
196
|
+
P: {
|
|
197
|
+
0: [7, 7, 7],
|
|
198
|
+
F: [[7, 7, 7]],
|
|
199
|
+
H: [[7, 7, 7]],
|
|
200
|
+
},
|
|
201
|
+
Q: [[5, 5, 5]],
|
|
202
|
+
R: {
|
|
203
|
+
0: [9, 9, 9],
|
|
204
|
+
Z: [[94, 94, 94], [94, 94, 94]],
|
|
205
|
+
S: [[94, 94, 94], [94, 94, 94]],
|
|
206
|
+
},
|
|
207
|
+
S: {
|
|
208
|
+
0: [4, 4, 4],
|
|
209
|
+
Z: { 0: [4, 4, 4], T: [[2, 43, 43]], C: { Z: [[2, 4, 4]], S: [[2, 4, 4]] }, D: [[2, 43, 43]] },
|
|
210
|
+
D: [[2, 43, 43]],
|
|
211
|
+
T: { 0: [2, 43, 43], R: { Z: [[2, 4, 4]], S: [[2, 4, 4]] }, C: { H: [[2, 4, 4]] }, S: { H: [[2, 4, 4]], C: { H: [[2, 4, 4]] } } },
|
|
212
|
+
C: { 0: [2, 4, 4], H: { 0: [4, 4, 4], T: { 0: [2, 43, 43], S: { C: { H: [[2, 4, 4]] }, H: [[2, 4, 4]] }, C: { H: [[2, 4, 4]] } }, D: [[2, 43, 43]] } },
|
|
213
|
+
H: { 0: [4, 4, 4], T: { 0: [2, 43, 43], C: { H: [[2, 4, 4]] }, S: { H: [[2, 4, 4]] } }, C: { H: [[2, 4, 4]] }, D: [[2, 43, 43]] },
|
|
214
|
+
},
|
|
215
|
+
T: {
|
|
216
|
+
0: [3, 3, 3],
|
|
217
|
+
C: { 0: [4, 4, 4], H: [[4, 4, 4]] },
|
|
218
|
+
Z: { 0: [4, 4, 4], S: [[4, 4, 4]] },
|
|
219
|
+
S: { 0: [4, 4, 4], Z: [[4, 4, 4]], H: [[4, 4, 4]], C: { H: [[4, 4, 4]] } },
|
|
220
|
+
T: { S: { 0: [4, 4, 4], Z: [[4, 4, 4]], C: { H: [[4, 4, 4]] } }, C: { H: [[4, 4, 4]] }, Z: [[4, 4, 4]] },
|
|
221
|
+
H: [[3, 3, 3]],
|
|
222
|
+
R: { Z: [[4, 4, 4]], S: [[4, 4, 4]] },
|
|
223
|
+
},
|
|
224
|
+
U: {
|
|
225
|
+
0: [0, -1, -1],
|
|
226
|
+
E: [[0, -1, -1]],
|
|
227
|
+
I: [[0, 1, -1]],
|
|
228
|
+
J: [[0, 1, -1]],
|
|
229
|
+
Y: [[0, 1, -1]],
|
|
230
|
+
},
|
|
231
|
+
V: [[7, 7, 7]],
|
|
232
|
+
W: [[7, 7, 7]],
|
|
233
|
+
X: [[5, 54, 54]],
|
|
234
|
+
Y: [[1, -1, -1]],
|
|
235
|
+
Z: {
|
|
236
|
+
0: [4, 4, 4],
|
|
237
|
+
D: { 0: [2, 43, 43], Z: { 0: [2, 4, 4], H: [[2, 4, 4]] } },
|
|
238
|
+
H: { 0: [4, 4, 4], D: { 0: [2, 43, 43], Z: { H: [[2, 4, 4]] } } },
|
|
239
|
+
S: { 0: [4, 4, 4], H: [[4, 4, 4]], C: { H: [[4, 4, 4]] } },
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
function translit(input) {
|
|
244
|
+
const map = {
|
|
245
|
+
'А': 'A',
|
|
246
|
+
'Б': 'B',
|
|
247
|
+
'В': 'V',
|
|
248
|
+
'Г': 'G',
|
|
249
|
+
'Д': 'D',
|
|
250
|
+
'Е': 'E',
|
|
251
|
+
'Ё': 'YO',
|
|
252
|
+
'Ж': 'ZH',
|
|
253
|
+
'З': 'Z',
|
|
254
|
+
'И': 'I',
|
|
255
|
+
'Й': 'I',
|
|
256
|
+
'К': 'K',
|
|
257
|
+
'Л': 'L',
|
|
258
|
+
'М': 'M',
|
|
259
|
+
'Н': 'N',
|
|
260
|
+
'О': 'O',
|
|
261
|
+
'П': 'P',
|
|
262
|
+
'Р': 'R',
|
|
263
|
+
'С': 'S',
|
|
264
|
+
'Т': 'T',
|
|
265
|
+
'У': 'U',
|
|
266
|
+
'Ф': 'F',
|
|
267
|
+
'Х': 'H',
|
|
268
|
+
'Ц': 'CZ',
|
|
269
|
+
'Ч': 'CH',
|
|
270
|
+
'Ш': 'SH',
|
|
271
|
+
'Щ': 'SCH',
|
|
272
|
+
'Ъ': '\'',
|
|
273
|
+
'Ы': 'I',
|
|
274
|
+
'Ь': '\'',
|
|
275
|
+
'Э': 'E',
|
|
276
|
+
'Ю': 'YU',
|
|
277
|
+
'Я': 'YA',
|
|
278
|
+
};
|
|
279
|
+
let result = '';
|
|
280
|
+
for (const char of input) {
|
|
281
|
+
result += map[char] ?? char;
|
|
282
|
+
}
|
|
283
|
+
// Постобработка: CZI → CI, CZE → CE и т.д.
|
|
284
|
+
return result
|
|
285
|
+
.replace(/CZI/g, 'CI')
|
|
286
|
+
.replace(/CZE/g, 'CE')
|
|
287
|
+
.replace(/CZY/g, 'CY')
|
|
288
|
+
.replace(/CZJ/g, 'CJ');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Подготавливает строку: очищает, приводит к верхнему регистру,
|
|
293
|
+
* транслитерирует кириллицу и удаляет не-буквенные символы.
|
|
294
|
+
*
|
|
295
|
+
* @since 0.4.0
|
|
296
|
+
*
|
|
297
|
+
**/
|
|
298
|
+
function prepareInput(input) {
|
|
299
|
+
if (!input)
|
|
300
|
+
return { word: undefined, isCyrillic: false };
|
|
301
|
+
let word = input
|
|
302
|
+
.trim()
|
|
303
|
+
.toUpperCase();
|
|
304
|
+
const isCyrillic = /[А-ЯЁ]/.test(word);
|
|
305
|
+
// Транслитерация кириллицы
|
|
306
|
+
if (isCyrillic)
|
|
307
|
+
word = translit(word);
|
|
308
|
+
// Удаляем всё, кроме букв
|
|
309
|
+
word = word.replace(/[^A-Z]/g, '');
|
|
310
|
+
return {
|
|
311
|
+
word: word,
|
|
312
|
+
isCyrillic,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Возвращает правило кодирования: [0] — для латиницы, [1] — для кириллицы.
|
|
317
|
+
*
|
|
318
|
+
* @since 0.4.0
|
|
319
|
+
*
|
|
320
|
+
**/
|
|
321
|
+
function getRule(node, isCyrillic) {
|
|
322
|
+
if (isCyrillic && '1' in node)
|
|
323
|
+
return node[1];
|
|
324
|
+
return node[0];
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Выбирает код из правила [начало, перед_гласной, иначе]:
|
|
328
|
+
* - Если в начале слова — [0]
|
|
329
|
+
* - Если следующий символ — гласная (A,E,I,O,U) — [1]
|
|
330
|
+
* - Иначе — [2]
|
|
331
|
+
*
|
|
332
|
+
* @since 0.4.0
|
|
333
|
+
*
|
|
334
|
+
**/
|
|
335
|
+
function selectCode(codeSet, word, position, offset) {
|
|
336
|
+
if (position === 0)
|
|
337
|
+
return codeSet[0];
|
|
338
|
+
const nextChar = word[position + offset];
|
|
339
|
+
if (nextChar && 'AEIOU'.includes(nextChar))
|
|
340
|
+
return codeSet[1];
|
|
341
|
+
return codeSet[2];
|
|
342
|
+
}
|
|
343
|
+
function padRight(str, length, char) {
|
|
344
|
+
while (str.length < length)
|
|
345
|
+
str += char;
|
|
346
|
+
return str.slice(0, length);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Упрощённая реализация алгоритма Daitch-Mokotoff для фонетического кодирования.
|
|
350
|
+
*
|
|
351
|
+
* Сложность: `O(n)` — один проход по строке.
|
|
352
|
+
*
|
|
353
|
+
* Преобразует строки в 6-значные коды, устойчивые к опечаткам, транслитерации и диалектным различиям.
|
|
354
|
+
* Поддерживает кириллицу через транслитерацию.
|
|
355
|
+
*
|
|
356
|
+
* @param input - Входная строка (может быть `null` / `undefined`)
|
|
357
|
+
* @returns 6-значный фонетический код
|
|
358
|
+
*
|
|
359
|
+
* @since 0.4.0
|
|
360
|
+
*
|
|
361
|
+
**/
|
|
362
|
+
function daitchMokotoffLite(input) {
|
|
363
|
+
const { word, isCyrillic } = prepareInput(input);
|
|
364
|
+
if (!word)
|
|
365
|
+
return '000000';
|
|
366
|
+
let i = 0;
|
|
367
|
+
let prev = -1; // Предыдущий добавленный код (для избежания дублей)
|
|
368
|
+
let lastNode;
|
|
369
|
+
let currentNode;
|
|
370
|
+
let result = '';
|
|
371
|
+
// Основной цикл: обработка по символам с учётом контекстных правил
|
|
372
|
+
while (i < word.length) {
|
|
373
|
+
// Начинаем с узла, соответствующего текущему символу
|
|
374
|
+
currentNode = lastNode = codes[word[i]];
|
|
375
|
+
let j = 1; // длина текущего совпавшего токена
|
|
376
|
+
// Поиск самого длинного совпадения (до 6 символов вперёд)
|
|
377
|
+
for (let k = 1; k < 7; k++) {
|
|
378
|
+
const char = word[i + k];
|
|
379
|
+
if (!char || !currentNode[char])
|
|
380
|
+
break;
|
|
381
|
+
currentNode = currentNode[char];
|
|
382
|
+
// Если текущий узел содержит правило (массив кодов) — фиксируем
|
|
383
|
+
if (currentNode[0]) {
|
|
384
|
+
lastNode = currentNode;
|
|
385
|
+
j = k + 1; // обновляем длину
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Выбираем код в зависимости от позиции и следующего символа
|
|
389
|
+
const code = selectCode(
|
|
390
|
+
// Выбираем правило: [0] — латиница, [1] — кириллица
|
|
391
|
+
getRule(lastNode, isCyrillic), word, i, j);
|
|
392
|
+
// console.log({
|
|
393
|
+
// word,
|
|
394
|
+
// char: word[i],
|
|
395
|
+
// rule: getRule(lastNode, isCyrillic),
|
|
396
|
+
// code,
|
|
397
|
+
// j,
|
|
398
|
+
// nextChar: word[i + j],
|
|
399
|
+
// })
|
|
400
|
+
// Добавляем код, только если он не -1 и не дублирует предыдущий
|
|
401
|
+
if (code !== -1 && code !== prev)
|
|
402
|
+
result += String(code);
|
|
403
|
+
prev = code;
|
|
404
|
+
i += j; // пропускаем обработанные символы
|
|
405
|
+
}
|
|
406
|
+
// Дополняем результат нулями до 6 символов
|
|
407
|
+
return padRight(result, 6, '0');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Строит множество триграмм для строки.
|
|
412
|
+
* Добавляет __ в начало и __ в конец.
|
|
413
|
+
*
|
|
414
|
+
* Пример: 'abc' → ['__A', '_AB', 'ABC', 'BC_', 'C__']
|
|
415
|
+
*
|
|
416
|
+
* @param input - входная строка
|
|
417
|
+
* @returns Record<TrigramChunk, boolean> — множество триграмм
|
|
418
|
+
*
|
|
419
|
+
* @since 0.4.0
|
|
420
|
+
*
|
|
421
|
+
**/
|
|
422
|
+
function buildTrigrams(input) {
|
|
423
|
+
const padded = `__${input.toUpperCase()}__`;
|
|
424
|
+
const chunks = {};
|
|
425
|
+
for (let i = 0; i < padded.length - 2; i++) {
|
|
426
|
+
chunks[padded.slice(i, i + 3)] = true;
|
|
427
|
+
}
|
|
428
|
+
return Object.freeze(chunks);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Вычисляет коэффициент подобия Жаккара между двумя наборами триграмм.
|
|
433
|
+
*
|
|
434
|
+
* Принимает:
|
|
435
|
+
* - строку (`from`) → построит триграммы
|
|
436
|
+
* - или уже построенные триграммы (`from`)
|
|
437
|
+
*
|
|
438
|
+
* @param from Исходная строка или её триграммы
|
|
439
|
+
* @param to Целевая строка
|
|
440
|
+
* @returns Коэффициент подобия [0..1]
|
|
441
|
+
*
|
|
442
|
+
* @since 0.4.0
|
|
443
|
+
*
|
|
444
|
+
**/
|
|
445
|
+
function trigramSimilarity(from, to) {
|
|
446
|
+
let fromTrigrams = {};
|
|
447
|
+
if (typeof from === 'string') {
|
|
448
|
+
if (from.length === 0 || to.length === 0)
|
|
449
|
+
return 0;
|
|
450
|
+
if (from === to)
|
|
451
|
+
return 1;
|
|
452
|
+
fromTrigrams = buildTrigrams(from);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
if (to.length === 0)
|
|
456
|
+
return 0;
|
|
457
|
+
fromTrigrams = from;
|
|
458
|
+
}
|
|
459
|
+
const toTrigrams = buildTrigrams(to);
|
|
460
|
+
let unionScore = 0;
|
|
461
|
+
let intersectionScore = 0;
|
|
462
|
+
for (const trigram in fromTrigrams) {
|
|
463
|
+
if (toTrigrams[trigram])
|
|
464
|
+
intersectionScore++;
|
|
465
|
+
unionScore++;
|
|
466
|
+
}
|
|
467
|
+
for (const trigram in toTrigrams) {
|
|
468
|
+
if (!fromTrigrams[trigram])
|
|
469
|
+
unionScore++;
|
|
470
|
+
}
|
|
471
|
+
if (!unionScore)
|
|
472
|
+
return 0;
|
|
473
|
+
return intersectionScore / unionScore;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const normalize = (input) => translit(input.toUpperCase());
|
|
477
|
+
function sumWeights(weights = {}) {
|
|
478
|
+
return (weights.phonetic ?? 0) + (weights.trigram ?? 0) + (weights.levenshtein ?? 0);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Предлагает наиболее близкое значение из списка допустимых,
|
|
482
|
+
* используя алгоритм Дамерау-Левенштейна для учёта опечаток.
|
|
483
|
+
*
|
|
484
|
+
* Функция предназначена для реализации подсказок вроде:
|
|
485
|
+
* "Вы имели в виду 'release'?", когда пользователь ввёл 'releas'.
|
|
486
|
+
*
|
|
487
|
+
* @param input Введённая пользователем строка, возможно, с опечаткой
|
|
488
|
+
* @param targetValues Список корректных, допустимых значений (например, команды, флаги)
|
|
489
|
+
* @param options Настройки нечёткого поиска
|
|
490
|
+
* @returns Наиболее близкое значение из `knownValues` или `undefined`, если совпадений нет
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* suggestClosest('releas', ['release', 'publish']) // → 'release'
|
|
495
|
+
* suggestClosest('релиз', ['release', 'publish']) // → 'release'
|
|
496
|
+
* ```
|
|
497
|
+
* @since 0.4.0
|
|
498
|
+
*
|
|
499
|
+
**/
|
|
500
|
+
function suggestClosest(input, targetValues, options = {}) {
|
|
501
|
+
const { maxDistance = 2, weights = { phonetic: 0.5, trigram: 0.3, levenshtein: 0.2 }, } = options;
|
|
502
|
+
const inputNorm = normalize(input);
|
|
503
|
+
let candidates = [];
|
|
504
|
+
if (!inputNorm)
|
|
505
|
+
return undefined;
|
|
506
|
+
for (const value of targetValues) {
|
|
507
|
+
// Ранний выход - прямое соответствие.
|
|
508
|
+
if (input === value)
|
|
509
|
+
return value;
|
|
510
|
+
const valueNorm = normalize(value);
|
|
511
|
+
// Ранний выход - прямое соответствие.
|
|
512
|
+
if (inputNorm === valueNorm)
|
|
513
|
+
return value;
|
|
514
|
+
candidates.push({
|
|
515
|
+
value: value,
|
|
516
|
+
valueNorm: valueNorm,
|
|
517
|
+
phonetic: daitchMokotoffLite(value),
|
|
518
|
+
triScore: 0,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
// Предвычисляем фонетический код и триграммы для ввода
|
|
522
|
+
const inputPhonetic = daitchMokotoffLite(input);
|
|
523
|
+
const inputTrigrams = buildTrigrams(inputNorm);
|
|
524
|
+
const phoneticCandidates = candidates.filter(c => c.phonetic === inputPhonetic);
|
|
525
|
+
// Замена кандидатов на фонетические, если есть прямые совпадения
|
|
526
|
+
// Если нет фонетических совпадений — используем все
|
|
527
|
+
//
|
|
528
|
+
if (phoneticCandidates.length > 0)
|
|
529
|
+
candidates = phoneticCandidates;
|
|
530
|
+
candidates = candidates.map((c) => {
|
|
531
|
+
c.triScore = trigramSimilarity(inputTrigrams, c.valueNorm);
|
|
532
|
+
return c;
|
|
533
|
+
});
|
|
534
|
+
const top10 = candidates
|
|
535
|
+
.sort((a, b) => b.triScore - a.triScore)
|
|
536
|
+
.slice(0, 10);
|
|
537
|
+
const totalWeight = sumWeights(weights);
|
|
538
|
+
const normWeight = (weight) => (totalWeight === 0 ? 0 : (weight ?? 0) / totalWeight);
|
|
539
|
+
let bestScore = -1;
|
|
540
|
+
let bestValue;
|
|
541
|
+
for (const candidate of top10) {
|
|
542
|
+
const { similarity: levScore } = damerauLevenshtein(inputNorm, candidate.valueNorm, maxDistance);
|
|
543
|
+
if (levScore <= 0)
|
|
544
|
+
continue; // Пропускаем, если расстояние слишком большое.
|
|
545
|
+
const phoneticScore = candidate.phonetic === inputPhonetic ? 1 : 0;
|
|
546
|
+
const score = phoneticScore * normWeight(weights.phonetic)
|
|
547
|
+
+ candidate.triScore * normWeight(weights.trigram)
|
|
548
|
+
+ levScore * normWeight(weights.levenshtein);
|
|
549
|
+
if (score > bestScore) {
|
|
550
|
+
bestScore = score;
|
|
551
|
+
bestValue = candidate.value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return bestValue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export { daitchMokotoffLite, damerauLevenshtein, suggestClosest };
|
package/dist/guards.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Проверяет, является ли переданное значение строковым примитивом (`string`).
|
|
3
|
+
*
|
|
4
|
+
* @param value - Значение, которое необходимо проверить.
|
|
5
|
+
* @returns `true`, если значение является строкой (`string`), иначе `false`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* isString('hello'); // true
|
|
10
|
+
* isString(123); // false
|
|
11
|
+
* ```
|
|
12
|
+
* @since 0.0.2
|
|
13
|
+
*
|
|
14
|
+
**/
|
|
15
|
+
const isString = (value) => typeof value === 'string';
|
|
16
|
+
/**
|
|
17
|
+
* Проверяет, является ли переданное значение числовым примитивом (`number`), не равным `NaN`.
|
|
18
|
+
*
|
|
19
|
+
* @param value - Значение, которое необходимо проверить.
|
|
20
|
+
* @returns `true`, если значение является числом (`number`) и не является `NaN`, иначе `false`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* isNumber(42) // true
|
|
25
|
+
* isNumber('42') // false
|
|
26
|
+
* isNumber(NaN) // false
|
|
27
|
+
* ```
|
|
28
|
+
* @since 0.0.2
|
|
29
|
+
*
|
|
30
|
+
**/
|
|
31
|
+
const isNumber = (value) => typeof value === 'number' && !isNaN(value);
|
|
32
|
+
/**
|
|
33
|
+
* Проверяет, является ли переданное значение булевым примитивом (`boolean`).
|
|
34
|
+
*
|
|
35
|
+
* @param value - Значение, которое необходимо проверить.
|
|
36
|
+
* @returns `true`, если значение является булевым примитивом (`true` или `false`), иначе `false`.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* Не возвращает `true` для объектов `Boolean`, даже если они обёрнуты.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* isBoolean(true) // true
|
|
45
|
+
* isBoolean(false) // true
|
|
46
|
+
* isBoolean(1) // false
|
|
47
|
+
* isBoolean('true') // false
|
|
48
|
+
* isBoolean(new Boolean(true)) // false (это объект, не примитив)
|
|
49
|
+
* ```
|
|
50
|
+
* @since 0.0.2
|
|
51
|
+
*
|
|
52
|
+
**/
|
|
53
|
+
function isBoolean(value) {
|
|
54
|
+
return typeof value === 'boolean';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Проверяет, является ли переданное значение функцией.
|
|
58
|
+
*
|
|
59
|
+
* @param value - Значение, которое необходимо проверить.
|
|
60
|
+
* @returns `true`, если значение является функцией (включая стрелочные функции, функции-объявления, методы и т.д.), иначе `false`.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* isFunction(() => {}); // true
|
|
65
|
+
* isFunction(function() {}); // true
|
|
66
|
+
* isFunction(Math.max); // true
|
|
67
|
+
* isFunction(class {}); // true (в JS классы — это функции)
|
|
68
|
+
* isFunction({}); // false
|
|
69
|
+
* isFunction('function'); // false
|
|
70
|
+
* ```
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*
|
|
73
|
+
**/
|
|
74
|
+
const isFunction = (value) => typeof value === 'function';
|
|
75
|
+
/**
|
|
76
|
+
* Проверяет, является ли значение объектом —
|
|
77
|
+
* имеет тип `object` и не равно `null`.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Проходят массивы и все объектные типы, включая
|
|
81
|
+
* встроенные (`Date`, `Map`, `Set`, `RegExp` и прочие).
|
|
82
|
+
*
|
|
83
|
+
* @param value - Проверяемое значение.
|
|
84
|
+
* @returns `true`, если это объект.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* isObject({}) // true
|
|
89
|
+
* isObject([]) // true
|
|
90
|
+
*
|
|
91
|
+
* isObject(() => {}) // false
|
|
92
|
+
* isObject(null) // false
|
|
93
|
+
* isObject('hello') // false
|
|
94
|
+
* isObject(42) // false
|
|
95
|
+
* ```
|
|
96
|
+
* @since 0.4.0
|
|
97
|
+
*/
|
|
98
|
+
const isObject = (value) => value !== null && typeof value === 'object';
|
|
99
|
+
/**
|
|
100
|
+
* Проверяет, является ли значение "обычным объектом" —
|
|
101
|
+
* имеет прототип `Object.prototype` или `null`.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* Не проходят массивы и встроенные типы (`Date`, `Map`, `Set`, `RegExp` и прочие).
|
|
105
|
+
*
|
|
106
|
+
* Не пройдёт и `Object.create({})`, т.к. экземпляр _унаследован_ от обычного объекта `{}`.
|
|
107
|
+
*
|
|
108
|
+
* @param value - Проверяемое значение.
|
|
109
|
+
*
|
|
110
|
+
* @returns `true`, если это обычный объект.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* isPlainObject({}) // true
|
|
115
|
+
* isPlainObject({ a: 1, b: 2 }) // true
|
|
116
|
+
* isPlainObject(Object.create(null)) // true
|
|
117
|
+
*
|
|
118
|
+
* isPlainObject(Object.create({})) // false
|
|
119
|
+
* isPlainObject(new Date()) // false
|
|
120
|
+
* isPlainObject(() => {}) // false
|
|
121
|
+
* isPlainObject([]) // false
|
|
122
|
+
* ```
|
|
123
|
+
* @since 0.4.0
|
|
124
|
+
*
|
|
125
|
+
**/
|
|
126
|
+
const isPlainObject = (value) => {
|
|
127
|
+
if (!isObject(value))
|
|
128
|
+
return false;
|
|
129
|
+
const proto = Object.getPrototypeOf(value);
|
|
130
|
+
return proto === null || proto === Object.prototype;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export { isNumber as a, isBoolean as b, isFunction as c, isObject as d, isPlainObject as e, isString as i };
|