Files
2025-12-13 23:24:46 +01:00

127 lines
4.2 KiB
JavaScript

// Simplified FuzzySet implementation for EPG matching
class FuzzySet {
constructor(arr, useLevenshtein = true, gramSizeLower = 2, gramSizeUpper = 3) {
this.exactSet = {};
this.matchDict = {};
this.items = {};
this.useLevenshtein = useLevenshtein;
this.gramSizeLower = gramSizeLower;
this.gramSizeUpper = gramSizeUpper;
if (arr) {
arr.forEach(item => this.add(item));
}
}
add(value) {
const normalized = this._normalize(value);
if (this.items[normalized]) return;
const index = Object.keys(this.items).length;
this.items[normalized] = [value, 0];
// Add to exact set for direct matching
this.exactSet[normalized] = [index, 0];
// Generate gram keys
const gramCount = this.gramSizeUpper - this.gramSizeLower + 1;
for (let gramSize = this.gramSizeLower; gramSize <= this.gramSizeUpper; gramSize++) {
const grams = this._gramify(normalized, gramSize);
grams.forEach(gram => {
if (!this.matchDict[gram]) this.matchDict[gram] = [];
this.matchDict[gram].push([index, gramCount]);
});
}
}
get(value) {
const normalized = this._normalize(value);
// Check exact match first
if (this.exactSet[normalized]) {
const [index, distance] = this.exactSet[normalized];
return [[1.0, this.items[normalized][0]]];
}
const results = [];
const gramResults = {};
const minGram = this.gramSizeLower;
const maxGram = this.gramSizeUpper;
// Get gram matches
for (let gramSize = minGram; gramSize <= maxGram; gramSize++) {
const grams = this._gramify(normalized, gramSize);
grams.forEach(gram => {
if (this.matchDict[gram]) {
this.matchDict[gram].forEach(([index, gramCount]) => {
if (!gramResults[index]) gramResults[index] = 0;
gramResults[index] += 1 / gramCount;
});
}
});
}
// Calculate scores
Object.keys(gramResults).forEach(index => {
const score = gramResults[index] / this._gramify(normalized, 2).length;
if (score > 0) {
results.push([score, this.items[Object.keys(this.items)[index]][0]]);
}
});
// Sort by score descending
results.sort((a, b) => b[0] - a[0]);
// Use Levenshtein distance for top results if enabled
if (this.useLevenshtein && results.length > 0) {
results.forEach(result => {
const normalizedItem = this._normalize(result[1]);
const distance = this._levenshtein(normalized, normalizedItem);
const maxLen = Math.max(normalized.length, normalizedItem.length);
result[0] = 1 - (distance / maxLen);
});
results.sort((a, b) => b[0] - a[0]);
}
return results.slice(0, 5); // Return top 5 matches
}
_normalize(str) {
return str.toLowerCase().replace(/[^\w\s]/g, ' ').replace(/\s+/g, ' ').trim();
}
_gramify(str, gramSize) {
const normalized = this._normalize(str);
const grams = [];
for (let i = 0; i < normalized.length - gramSize + 1; i++) {
grams.push(normalized.substring(i, i + gramSize));
}
return grams;
}
_levenshtein(a, b) {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
const cost = a[j - 1] === b[i - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
);
}
}
return matrix[b.length][a.length];
}
}