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

115 lines
3.4 KiB
Dart

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<String> _massUnits = {'mg', 'g', 'kg', 'oz', 'lb'};
/// Units expressed in millilitres at the base scale.
const Set<String> _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<String, String> _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<String> 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();
}