import 'ingredient_name.dart'; /// Physical dimension a unit belongs to. Quantities are only summed (and /// subtracted from pantry stock) within the same dimension — grams never /// cancel against millilitres or item counts. enum UnitDimension { mass, volume, count } /// Units expressed in grams at the base scale. const Set _massUnits = {'mg', 'g', 'kg', 'oz', 'lb'}; /// Units expressed in millilitres at the base scale. const Set _volumeUnits = {'ml', 'l', 'tsp', 'tbsp', 'cup', 'floz'}; /// Synonyms applied after lowercasing + singularizing a raw unit token /// (`grams` → `gram` → `g`, `lbs` → `lb`, `pieces` → `count`). const Map _unitSynonyms = { 'milligram': 'mg', 'gram': 'g', 'kilogram': 'kg', 'ounce': 'oz', 'pound': 'lb', 'milliliter': 'ml', 'liter': 'l', 'teaspoon': 'tsp', 'tablespoon': 'tbsp', 'fluidounce': 'floz', 'piece': 'count', 'unit': 'count', 'each': 'count', 'item': 'count', }; /// Canonical unit spellings offered by the pantry/shopping dialogs. const List pantryUnitOptions = [ 'count', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'tsp', 'tbsp', 'cup', ]; /// Canonicalizes a raw unit token (recipe parser output, dialog selection, or /// persisted value). `null`/empty defaults to the generic `count`. String canonicalUnit(String? raw) { if (raw == null) return 'count'; final trimmed = raw.trim().toLowerCase(); if (trimmed.isEmpty) return 'count'; final singular = singularizeWord(trimmed); return _unitSynonyms[singular] ?? singular; } /// Dimension [canonical] belongs to (see [UnitDimension]). UnitDimension unitDimension(String canonical) { if (_massUnits.contains(canonical)) return UnitDimension.mass; if (_volumeUnits.contains(canonical)) return UnitDimension.volume; return UnitDimension.count; } /// Multiplicative factor that converts one [canonical] unit into the base /// unit of its dimension (g / ml / each). double _factorToBase(String canonical) { switch (canonical) { case 'mg': return 0.001; case 'kg': return 1000; case 'oz': return 28.349523125; case 'lb': return 453.59237; case 'l': return 1000; case 'floz': return 29.5735295625; case 'tsp': return 4.92892159375; case 'tbsp': return 14.78676478125; case 'cup': return 236.5882365; case 'dozen': return 12; default: // `g`, `ml`, `count` and custom count-style units are their own base. return 1; } } /// Converts [quantity] of [canonical] into the base unit of its dimension /// (grams / millilitres / each), enabling cross-unit aggregation such as /// `0.5 lb + 226.8 g`. double unitsToBase(double quantity, String canonical) => quantity * _factorToBase(canonical); /// Inverse of [unitsToBase]: expresses [baseQuantity] in [canonical] units. double unitsFromBase(double baseQuantity, String canonical) { final factor = _factorToBase(canonical); return factor == 0 ? baseQuantity : baseQuantity / factor; } /// Rounds derived quantities to 2 decimals for stable storage and display. double roundTo2(double value) => (value * 100).roundToDouble() / 100; /// Compact display formatting for quantities: up to 2 decimals with trailing /// zeros trimmed (e.g. `500`, `0.75`, `1.5`). String formatQuantityForDisplay(double value) { final rounded = roundTo2(value); if (rounded == rounded.roundToDouble()) return rounded.round().toString(); return rounded.toString(); }