@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.
@@ -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 };
@@ -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 };