@stonyx/utils 0.2.3-beta.19 → 0.2.3-beta.20

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,26 @@
1
+ /**
2
+ * Generic fuzzy string matching for cross-source reconciliation.
3
+ * Handles Unicode normalization, stop-word filtering, and word-set similarity scoring.
4
+ */
5
+ export interface FuzzyMatchOptions {
6
+ stopWords?: string[];
7
+ delimiter?: string;
8
+ threshold?: number;
9
+ }
10
+ export interface FuzzyMatchResult<T extends {
11
+ name: string;
12
+ }> {
13
+ entry: T;
14
+ score: number;
15
+ }
16
+ export default class FuzzyMatch {
17
+ stopWords: string[];
18
+ delimiter: string;
19
+ threshold: number;
20
+ constructor(options?: FuzzyMatchOptions);
21
+ normalize(name: string): string;
22
+ similarity(nameA: string, nameB: string): number;
23
+ findBestMatch<T extends {
24
+ name: string;
25
+ }>(nameA: string, nameB: string, entries: T[], threshold?: number): FuzzyMatchResult<T> | null;
26
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Generic fuzzy string matching for cross-source reconciliation.
3
+ * Handles Unicode normalization, stop-word filtering, and word-set similarity scoring.
4
+ */
5
+ function normalizeString(name, stopWords = []) {
6
+ let result = name
7
+ .normalize('NFD')
8
+ .replace(/[\u0300-\u036f]/g, '')
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9\s]/g, ' ');
11
+ if (stopWords.length) {
12
+ const pattern = new RegExp(`\\b(${stopWords.join('|')})\\b`, 'g');
13
+ result = result.replace(pattern, '');
14
+ }
15
+ return result.replace(/\s+/g, ' ').trim();
16
+ }
17
+ function wordSet(normalized) {
18
+ return new Set(normalized.split(' ').filter(w => w.length > 1));
19
+ }
20
+ export default class FuzzyMatch {
21
+ stopWords;
22
+ delimiter;
23
+ threshold;
24
+ constructor(options = {}) {
25
+ this.stopWords = options.stopWords || [];
26
+ this.delimiter = options.delimiter || '\u00B7';
27
+ this.threshold = options.threshold || 0.35;
28
+ }
29
+ normalize(name) {
30
+ return normalizeString(name, this.stopWords);
31
+ }
32
+ similarity(nameA, nameB) {
33
+ const aN = this.normalize(nameA);
34
+ const sN = this.normalize(nameB);
35
+ if (!aN || !sN)
36
+ return 0;
37
+ if (aN === sN)
38
+ return 1.0;
39
+ if (aN.includes(sN) || sN.includes(aN))
40
+ return 0.9;
41
+ const aWords = wordSet(aN);
42
+ const sWords = wordSet(sN);
43
+ if (aWords.size === 0 || sWords.size === 0)
44
+ return 0;
45
+ let overlap = 0;
46
+ for (const w of aWords) {
47
+ if (sWords.has(w)) {
48
+ overlap++;
49
+ }
50
+ else {
51
+ for (const sw of sWords) {
52
+ if (sw.startsWith(w) || w.startsWith(sw)) {
53
+ overlap += 0.7;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return overlap / Math.max(aWords.size, sWords.size);
60
+ }
61
+ findBestMatch(nameA, nameB, entries, threshold) {
62
+ const minScore = threshold ?? this.threshold;
63
+ let bestMatch = null;
64
+ let bestScore = 0;
65
+ for (const entry of entries) {
66
+ const parts = entry.name.split(this.delimiter);
67
+ if (parts.length !== 2)
68
+ continue;
69
+ const [entryA, entryB] = parts;
70
+ const normalScore = (this.similarity(nameA, entryA) + this.similarity(nameB, entryB)) / 2;
71
+ const reversedScore = (this.similarity(nameA, entryB) + this.similarity(nameB, entryA)) / 2;
72
+ const score = Math.max(normalScore, reversedScore);
73
+ if (score > bestScore) {
74
+ bestScore = score;
75
+ bestMatch = entry;
76
+ }
77
+ }
78
+ return bestScore >= minScore && bestMatch ? { entry: bestMatch, score: bestScore } : null;
79
+ }
80
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.3-beta.19",
6
+ "version": "0.2.3-beta.20",
7
7
  "description": "Utils module for Stonyx Framework",
8
8
  "repository": {
9
9
  "type": "git",
@@ -38,6 +38,10 @@
38
38
  "./string": {
39
39
  "types": "./dist/string.d.ts",
40
40
  "default": "./dist/string.js"
41
+ },
42
+ "./fuzzy-match": {
43
+ "types": "./dist/fuzzy-match.d.ts",
44
+ "default": "./dist/fuzzy-match.js"
41
45
  }
42
46
  },
43
47
  "publishConfig": {