115 lines
3.4 KiB
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();
|
|
} |