e047d884b6
Backend (uv): FastAPI app with routers (foods, log, targets, OFF proxy), SQLAlchemy models, Pydantic schemas, migration runner + 0001 initial schema, example pytest suite (7 tests). Frontend (npm): Vite 7 + Svelte 5 runes, lib/ (api, stores, scanner, format) per spec §7, placeholder components, example vitest suite (4 tests). SPEC.md §1: registered uvicorn and Vitest (rule §8.3.3).
31 lines
1.2 KiB
Python
31 lines
1.2 KiB
Python
"""Nutrition math — the ONLY place nutrition is computed (spec §8.1 rule 1).
|
||
|
||
Routers never compute nutrition; the frontend never re-derives it.
|
||
`quantity` is interpreted by context (spec §2.1):
|
||
- unit_type "weight": quantity is grams; nutrition = (quantity / 100) × per_unit
|
||
- unit_type "count": quantity is item count; nutrition = quantity × per_unit
|
||
- meal: quantity is a scaling factor (1.0 = one full meal)
|
||
"""
|
||
|
||
from models import Food
|
||
|
||
|
||
def scale_to_quantity(per_unit: float | None, quantity: float, unit_type: str) -> float:
|
||
"""Scale a per-unit nutrition value to a logged quantity. Missing values count as 0."""
|
||
if per_unit is None:
|
||
return 0.0
|
||
if unit_type == "weight":
|
||
return per_unit * quantity / 100.0
|
||
if unit_type == "count":
|
||
return per_unit * quantity
|
||
raise ValueError(f"unknown unit_type: {unit_type!r}")
|
||
|
||
|
||
def entry_calories(food: Food, quantity: float) -> float:
|
||
"""Calories for a single (non-meal) food at a logged quantity.
|
||
|
||
TODO: handle is_meal foods by summing scaled component nutrition
|
||
(recursively, with cycle detection — spec §2.2).
|
||
"""
|
||
return scale_to_quantity(food.calories_per_unit, quantity, food.unit_type)
|