glp-companion/lib/features/pantry/domain/value_objects/ingredient_name.dart

48 lines
2.0 KiB
Dart

/// Shared English-singularization used to fold plural ingredient and unit
/// strings onto one canonical token (`eggs` → `egg`, `lbs` → `lb`,
/// `chicken breasts` → `chicken breast`).
///
/// Rules are intentionally conservative and applied identically to both sides
/// of a comparison (recipe line vs. pantry row), so even imperfect results
/// still match as long as they are consistent.
String singularizeWord(String word) {
if (word.length <= 2) return word;
// berries → berry.
if (word.endsWith('ies') && word.length > 4) {
return '${word.substring(0, word.length - 3)}y';
}
// class / glass — never strip a double s.
if (word.endsWith('ss')) return word;
// bunches → bunch, boxes → box, buses → bus, tomatoes → tomato.
if (word.endsWith('ches') ||
word.endsWith('shes') ||
word.endsWith('xes') ||
word.endsWith('zes') ||
word.endsWith('ses') ||
word.endsWith('oes')) {
return word.substring(0, word.length - 2);
}
// status / asparagus keep their s.
if (word.endsWith('us') || word.endsWith('is')) return word;
// eggs → egg.
if (word.endsWith('s')) return word.substring(0, word.length - 1);
return word;
}
/// Canonical, lowercase ingredient key used for fuzzy pantry ↔ recipe
/// matching (e.g. `Chicken Breast (boneless)` → `chicken breast`).
///
/// The pipeline: trim/lowercase → drop parenthetical qualifiers → collapse
/// punctuation to spaces → singularize each token.
String canonicalIngredientName(String raw) {
var text = raw.trim().toLowerCase();
if (text.isEmpty) return '';
// Drop parenthetical qualifiers: "chicken breast (boneless)" → "chicken breast".
text = text.replaceAll(RegExp(r'\([^)]*\)'), ' ');
// Collapse punctuation so "chicken-breast" and "chicken, breast" still
// match the plain "chicken breast" pantry key.
text = text.replaceAll(RegExp(r'[^a-z0-9]+'), ' ');
text = text.replaceAll(RegExp(r'\s+'), ' ').trim();
if (text.isEmpty) return '';
return text.split(' ').map(singularizeWord).join(' ');
}