mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +01:00
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Has been cancelled
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Has been cancelled
Tests / test (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
126 lines
4.2 KiB
Python
126 lines
4.2 KiB
Python
"""Weekly-average planner: what to eat on the rest of the week's days.
|
|
|
|
"I want to average N kcal this week, and on Monday I ate 2500 -- what do I
|
|
eat on the other days?" The week is Monday-Sunday around ``today``. A day is
|
|
*known* when the user typed a number for it, or when it is already over
|
|
(before ``today``) and has meals in the log. Every other day -- today
|
|
included, since it is still in progress -- shares what is left evenly::
|
|
|
|
per_day = (target x 7 - sum(known)) / remaining_days
|
|
|
|
Informational only, like the calorie table: it never touches the budget.
|
|
Pure over its arguments; mirrored in ``app/lib/services/week_plan.dart`` and
|
|
gated by the shared ``tests/fixtures/body_calc.json``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date, timedelta
|
|
|
|
from diet_guard._body_calc import half_up
|
|
from diet_guard._body_energy import MIN_TARGET_KCAL
|
|
|
|
__all__ = ["WEEKDAYS", "DayPlan", "WeekPlan", "plan_week", "week_start"]
|
|
|
|
WEEKDAYS: tuple[str, ...] = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DayPlan:
|
|
"""One day of the plan.
|
|
|
|
Attributes:
|
|
day: ``YYYY-MM-DD``.
|
|
weekday: ``Mon`` .. ``Sun``.
|
|
kcal: What was eaten (known) or should be eaten (planned).
|
|
source: ``typed``, ``logged``, ``plan``, or ``unlogged`` -- a day
|
|
already over with nothing in the log, planned like a remaining
|
|
day (an unlogged day is not a zero-kcal day) so the screens can
|
|
ask for the real number.
|
|
"""
|
|
|
|
day: str
|
|
weekday: str
|
|
kcal: int
|
|
source: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WeekPlan:
|
|
"""The whole week.
|
|
|
|
Attributes:
|
|
days: Monday .. Sunday.
|
|
target_avg: The weekly average aimed for.
|
|
per_remaining: kcal for each ``plan`` day, or None when none remain.
|
|
week_avg: The average the plan produces (equals ``target_avg`` up to
|
|
rounding when anything remains; the achieved average otherwise).
|
|
"""
|
|
|
|
days: tuple[DayPlan, ...]
|
|
target_avg: int
|
|
per_remaining: int | None
|
|
week_avg: float
|
|
|
|
@property
|
|
def below_floor(self) -> bool:
|
|
"""Whether the remaining days would have to go under 1200 kcal."""
|
|
return self.per_remaining is not None and self.per_remaining < MIN_TARGET_KCAL
|
|
|
|
|
|
def _open_source(monday: date, index: int, today: date) -> str:
|
|
"""``unlogged`` for a past day without data, ``plan`` for today onward."""
|
|
return "unlogged" if monday + timedelta(days=index) < today else "plan"
|
|
|
|
|
|
def week_start(today: date) -> date:
|
|
"""The Monday of ``today``'s week."""
|
|
return today - timedelta(days=today.weekday())
|
|
|
|
|
|
def plan_week(
|
|
today: date,
|
|
target_avg: int,
|
|
logged: dict[str, float],
|
|
typed: dict[int, int],
|
|
) -> WeekPlan:
|
|
"""Plan the rest of ``today``'s week.
|
|
|
|
Args:
|
|
today: The current local date.
|
|
target_avg: Desired mean kcal/day over the seven days.
|
|
logged: ``YYYY-MM-DD`` -> kcal eaten, from the food log. Only days
|
|
before ``today`` are used.
|
|
typed: Weekday index (0 = Monday) -> kcal the user typed; wins over
|
|
the log, and may name any day (a planned feast on Saturday too).
|
|
"""
|
|
monday = week_start(today)
|
|
known: dict[int, tuple[int, str]] = {}
|
|
for index in range(7):
|
|
day = monday + timedelta(days=index)
|
|
if index in typed:
|
|
known[index] = (typed[index], "typed")
|
|
elif day < today and day.isoformat() in logged:
|
|
known[index] = (int(half_up(logged[day.isoformat()])), "logged")
|
|
remaining = 7 - len(known)
|
|
eaten = sum(kcal for kcal, _ in known.values())
|
|
per = int(half_up((target_avg * 7 - eaten) / remaining)) if remaining else None
|
|
days = tuple(
|
|
DayPlan(
|
|
day=(monday + timedelta(days=index)).isoformat(),
|
|
weekday=WEEKDAYS[index],
|
|
kcal=known[index][0] if index in known else int(per or 0),
|
|
source=known[index][1]
|
|
if index in known
|
|
else _open_source(monday, index, today),
|
|
)
|
|
for index in range(7)
|
|
)
|
|
return WeekPlan(
|
|
days=days,
|
|
target_avg=target_avg,
|
|
per_remaining=per,
|
|
week_avg=sum(d.kcal for d in days) / 7,
|
|
)
|