costco-grocery-list/frontend/src/utils/stringSimilarity.js
Nico 7c58ab57f7
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 12s
Build & Deploy Costco Grocery List / deploy (push) Successful in 5s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
fix string similarity check
2026-01-02 18:51:44 -08:00

77 lines
2.5 KiB
JavaScript

/**
* Calculate Levenshtein distance between two strings
* @param {string} str1 - First string
* @param {string} str2 - Second string
* @returns {number} - Edit distance
*/
function levenshteinDistance(str1, str2) {
const len1 = str1.length;
const len2 = str2.length;
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
return matrix[len1][len2];
}
/**
* Calculate similarity percentage between two strings (0-100%)
* @param {string} str1 - First string
* @param {string} str2 - Second string
* @returns {number} - Similarity percentage
*/
export function calculateSimilarity(str1, str2) {
const lower1 = str1.toLowerCase().trim();
const lower2 = str2.toLowerCase().trim();
if (lower1 === lower2) return 100;
if (lower1.length === 0 || lower2.length === 0) return 0;
// Check if one string contains the other (substring match)
if (lower1.includes(lower2) || lower2.includes(lower1)) {
// Give high similarity for substring matches
const minLen = Math.min(lower1.length, lower2.length);
const maxLen = Math.max(lower1.length, lower2.length);
return Math.round((minLen / maxLen) * 100);
}
const distance = levenshteinDistance(lower1, lower2);
const maxLength = Math.max(lower1.length, lower2.length);
const similarity = ((maxLength - distance) / maxLength) * 100;
return Math.round(similarity);
}
/**
* Find items with similarity >= threshold
* @param {string} inputName - Item name to check
* @param {Array} existingItems - Array of existing items with item_name property
* @param {number} threshold - Minimum similarity percentage (default 80)
* @returns {Array} - Array of similar items sorted by similarity
*/
export function findSimilarItems(inputName, existingItems, threshold = 70) {
const similar = [];
for (const item of existingItems) {
const similarity = calculateSimilarity(inputName, item.item_name);
if (similarity >= threshold && similarity < 100) {
similar.push({ ...item, similarity });
}
}
// Sort by similarity descending
return similar.sort((a, b) => b.similarity - a.similarity);
}