Files
MealQuest/web/src/dinners/names.ts
T
orchestrator-agent a3648397ee
CI / ci (pull_request) Successful in 2m26s
CI / ci (push) Successful in 2m38s
Fix docker build: move dedupe cases fixture into web (#99)
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
2026-10-06 21:51:48 -04:00

59 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The PWA's copy of the server's dinner-name rules (internal/ai/harness
// Normalize and Similar), so a screen can tell beforehand that the dinners
// API would refuse a name as a duplicate. The server still decides.
// web/src/dinners/testdata/dedupe_cases.json is run against both.
import folds from './letterFolds.json'
// SIMILAR_THRESHOLD is harness.SimilarThreshold.
const SIMILAR_THRESHOLD = 0.85
// fillerWords is harness's fillerWords.
const fillerWords = new Set(['easy', 'best', 'homemade', 'quick', 'simple', 'the', 'a', 'recipe', 'classic'])
// letterFolds is harness's letterFolds: gen_folds.py writes both tables,
// and harness's TestLetterFoldsMatchPWA fails when they differ.
const letterFolds: Record<string, string> = folds
const combiningMark = /\p{Mn}/u
const wordChar = /[\p{L}\p{Nd}]/u
// normalizeName is harness.Normalize: lowercased, accents dropped, letters
// folded, apostrophes removed, other punctuation turned into spaces and
// filler words dropped. "Easy Chicken Tikka-Masala!" is
// "chicken tikka masala".
export function normalizeName(name: string): string {
let out = ''
// Lowercased letter by letter, as Go's strings.ToLower does, so no
// letter is lowered by its context (a final Σ is σ, not ς).
for (const letter of name) {
for (const ch of letter.toLowerCase()) {
if (combiningMark.test(ch) || ch === "'" || ch === '’') continue
const fold = letterFolds[ch]
if (fold !== undefined) out += fold
else if (wordChar.test(ch)) out += ch
else out += ' '
}
}
return out
.split(' ')
.filter((word) => word !== '' && !fillerWords.has(word))
.join(' ')
}
function words(name: string): Set<string> {
const normalized = normalizeName(name)
return new Set(normalized === '' ? [] : normalized.split(' '))
}
// similarNames is harness.Similar: whether a and b name the same dinner, by
// the share of the words of their normalized forms they have in common.
export function similarNames(a: string, b: string): boolean {
const left = words(a)
const right = words(b)
if (left.size === 0 || right.size === 0) return false
let shared = 0
for (const word of left) if (right.has(word)) shared++
return shared / (left.size + right.size - shared) >= SIMILAR_THRESHOLD
}