Public Access
feat(backend): implement unit conversion for cost calculation
- Add UnitConverter (normalization, within-family, density tables) - Update cost.py to convert recipe qty to grocery price unit - Update generate.py _load_match_index to fetch ingredient name + unit - Fix orchestrator email/shopping-list cost loops to use conversion - Fix missing Ingredient import in generate.py - Add 19 unit tests
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Tests for app.utils.units."""
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.units import UnitConverter, convert_qty, normalize_unit
|
||||
|
||||
|
||||
class TestNormalizeUnit:
|
||||
def test_synonyms(self):
|
||||
assert normalize_unit("pounds") == "lb"
|
||||
assert normalize_unit("OUNCE") == "oz"
|
||||
assert normalize_unit("Cups") == "cup"
|
||||
assert normalize_unit("TBS") == "tbsp"
|
||||
assert normalize_unit("each") == "ea"
|
||||
assert normalize_unit("doz") == "dozen"
|
||||
|
||||
def test_already_canonical(self):
|
||||
assert normalize_unit("lb") == "lb"
|
||||
assert normalize_unit("cup") == "cup"
|
||||
assert normalize_unit("ea") == "ea"
|
||||
|
||||
def test_unknown_passthrough(self):
|
||||
assert normalize_unit("bunch") == "bunch"
|
||||
assert normalize_unit("can") == "can"
|
||||
|
||||
def test_none(self):
|
||||
assert normalize_unit(None) == "unknown"
|
||||
assert normalize_unit("") == "unknown"
|
||||
|
||||
|
||||
class TestWithinFamilyConversion:
|
||||
def test_weight_lb_to_oz(self):
|
||||
assert convert_qty(2.0, "lb", "oz") == Decimal("32")
|
||||
|
||||
def test_weight_oz_to_lb(self):
|
||||
result = convert_qty(16.0, "oz", "lb")
|
||||
assert result == Decimal("1.0000")
|
||||
|
||||
def test_volume_cup_to_tbsp(self):
|
||||
result = convert_qty(1.0, "cup", "tbsp")
|
||||
assert result == Decimal("16")
|
||||
|
||||
def test_volume_tbsp_to_tsp(self):
|
||||
result = convert_qty(1.0, "tbsp", "tsp")
|
||||
assert result == Decimal("3")
|
||||
|
||||
def test_count_dozen_to_ea(self):
|
||||
result = convert_qty(1.0, "dozen", "ea")
|
||||
assert result == Decimal("12")
|
||||
|
||||
def test_same_unit(self):
|
||||
assert convert_qty(5.0, "lb", "lb") == Decimal("5")
|
||||
|
||||
|
||||
class TestDensityConversion:
|
||||
def test_olive_oil_tbsp_to_cup(self):
|
||||
# 1 cup = 16 tbsp
|
||||
result = convert_qty(
|
||||
1.0, "cup", "tbsp", ingredient_name_lower="olive oil"
|
||||
)
|
||||
assert result == Decimal("16")
|
||||
|
||||
def test_rice_cup_to_lb(self):
|
||||
# 1 cup rice = 185 g; 1 lb = 453.592 g → 1 cup = 185/453.592 ≈ 0.4079 lb
|
||||
result = convert_qty(
|
||||
1.0, "cup", "lb", ingredient_name_lower="rice, long-grain white"
|
||||
)
|
||||
assert result == Decimal("0.4079")
|
||||
|
||||
def test_butter_tbsp_to_cup(self):
|
||||
result = convert_qty(
|
||||
1.0, "cup", "tbsp", ingredient_name_lower="butter, unsalted"
|
||||
)
|
||||
assert result == Decimal("16")
|
||||
|
||||
def test_seed_size_ea_to_lb(self):
|
||||
# 1 avocado ≈ 170 g = 0.3748 lb
|
||||
result = convert_qty(
|
||||
1.0, "ea", "lb", ingredient_name_lower="avocado"
|
||||
)
|
||||
assert result == Decimal("0.3748")
|
||||
|
||||
def test_unknown_density_falls_back(self):
|
||||
# No density for "dragon fruit" → fallback to dimensionless qty
|
||||
result = convert_qty(2.0, "cup", "lb", ingredient_name_lower="dragon fruit")
|
||||
assert result == Decimal("2")
|
||||
|
||||
|
||||
class TestFallback:
|
||||
def test_incommensurable_units_without_name(self):
|
||||
result = convert_qty(2.0, "cup", "lb")
|
||||
assert result == Decimal("2")
|
||||
|
||||
def test_incommensurable_units_with_unmapped_name(self):
|
||||
result = convert_qty(2.0, "cup", "lb", ingredient_name_lower="xyz")
|
||||
assert result == Decimal("2")
|
||||
|
||||
def test_missing_from_unit(self):
|
||||
result = convert_qty(2.0, None, "lb")
|
||||
assert result == Decimal("2")
|
||||
|
||||
def test_missing_to_unit(self):
|
||||
result = convert_qty(2.0, "lb", None)
|
||||
assert result == Decimal("2")
|
||||
Reference in New Issue
Block a user