Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Each kind of knowledge has one home. Write a change in the home that matches it;
| Code gen (watch) | `cd menu_management && dart run build_runner watch --delete-conflicting-outputs` | |
| Build release | `cd menu_management && flutter build windows` | |
| Build + copy to Desktop | `./build_and_copy.bat` | Run from repo root; it `cd`s into `menu_management` and runs `build_and_copy.ps1`. Windows only; copies portable build to Desktop |
| Run all tests | `cd menu_management && flutter test test/` | 1050 tests across 42 files |
| Run all tests | `cd menu_management && flutter test test/` | 1053 tests across 42 files |
| Run single test | `cd menu_management && flutter test test/<file>.dart` | |
| List devices | `flutter devices` | |
| Format check | `cd menu_management && find lib test -name "*.dart" ! -name "*.freezed.dart" ! -name "*.g.dart" -print0 \| xargs -0 dart format --set-exit-if-changed` | Bash/Git Bash; excludes generated files; fix drift by re-running without `--set-exit-if-changed` |
Expand Down
2 changes: 1 addition & 1 deletion adr/0014-multi-trip-shopping-planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ When no trip can possibly serve a perishable event fresh (very short shelf life

### Cross-week leftovers

A cook event late in week N can feed a leftover meal in week N+1. The food of that meal is bought with the cook event, because the cook cooks it on the cook day. `buildCookingTimeline` puts the whole amount of a cook event on its cook day, so the planner assigns it to the trip of the cook event. The shopping PDF follows the same rule: a trip section lists a leftover meal under the trip of its cook event (`IngredientMealRequirement.cookWeekIndex`), not under the trip of the week of the meal.
A cook event late in week N can feed a leftover meal in week N+1. The food of that meal is bought with the cook event, because the cook cooks it on the cook day. `buildCookingTimeline` puts the whole amount of a cook event on its cook day, so the planner assigns it to the trip of the cook event. The shopping PDF follows the same rule at the level of the day. `TripItem.cookDays` records the cook days that each trip item buys for. A trip section lists a meal under the trip whose items hold its cook day (`IngredientMealRequirement.cookDayIndex`). A rule by week range is not enough, because the planner can put a long-life item of a later week on an earlier trip (see also ADR 0015). A cook day that no trip buys, because owned stock covers it, goes under the first trip that buys the ingredient.

### Per-event shelf life lookup

Expand Down
28 changes: 14 additions & 14 deletions menu_management/lib/menu/models/multi_week_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
///
/// The shopping totals ([allIngredients], [ingredientSources]) and the per-meal breakdown
/// ([ingredientMealRequirements]) all read this list, so they count the same meals.
List<({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookWeekIndex})> _fedSubMeals({
List<({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookDayIndex})> _fedSubMeals({
required List<Recipe> recipes,
}) {
List<({int weekIndex, Meal meal})> ordered = [
Expand All @@ -201,9 +201,9 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
return a.meal.mealTime.goesBefore(b.meal.mealTime) ? -1 : 1;
});

// The latest cook event of each recipe so far: its absolute day and its week.
Map<String, ({int day, int weekIndex})> latestCook = {};
List<({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookWeekIndex})> fed = [];
// The absolute day of the latest cook event of each recipe so far.
Map<String, int> latestCook = {};
List<({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookDayIndex})> fed = [];

for (({int weekIndex, Meal meal}) entry in ordered) {
int day = entry.weekIndex * 7 + entry.meal.mealTime.weekDay.value;
Expand All @@ -214,22 +214,22 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
Recipe? recipe = recipes.firstWhereOrNull((Recipe r) => r.id == cooking.recipeId);
if (recipe == null) continue;

int cookWeekIndex;
int cookDayIndex;
if (cooking.yield > 0) {
latestCook[recipe.id] = (day: day, weekIndex: entry.weekIndex);
cookWeekIndex = entry.weekIndex;
latestCook[recipe.id] = day;
cookDayIndex = day;
} else {
({int day, int weekIndex})? cook = latestCook[recipe.id];
if (cook == null || day - cook.day > recipe.maxStorageDays) continue;
cookWeekIndex = cook.weekIndex;
int? cookDay = latestCook[recipe.id];
if (cookDay == null || day - cookDay > recipe.maxStorageDays) continue;
cookDayIndex = cookDay;
}
fed.add((
weekIndex: entry.weekIndex,
mealTime: entry.meal.mealTime,
subMealIndex: subMealIndex,
subMeal: subMeal,
recipe: recipe,
cookWeekIndex: cookWeekIndex,
cookDayIndex: cookDayIndex,
));
}
}
Expand All @@ -240,7 +240,7 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
/// appears in [_fedSubMeals]. This number is the servings that the whole menu cooks of the recipe.
List<({Recipe recipe, int servings})> _servingsPerRecipe({required List<Recipe> recipes}) {
Map<String, ({Recipe recipe, int servings})> byRecipeId = {};
for (({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookWeekIndex}) fed in _fedSubMeals(
for (({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookDayIndex}) fed in _fedSubMeals(
recipes: recipes,
)) {
int servings = byRecipeId[fed.recipe.id]?.servings ?? 0;
Expand Down Expand Up @@ -306,7 +306,7 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
Map<String, List<IngredientMealRequirement>> ingredientMealRequirements({required List<Recipe> recipes}) {
Map<String, List<IngredientMealRequirement>> requirements = {};

for (({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookWeekIndex}) fed in _fedSubMeals(
for (({int weekIndex, MealTime mealTime, int subMealIndex, SubMeal subMeal, Recipe recipe, int cookDayIndex}) fed in _fedSubMeals(
recipes: recipes,
)) {
for (MapEntry<String, List<Quantity>> ingredient in fed.recipe.perServingQuantities().entries) {
Expand All @@ -315,7 +315,7 @@ abstract class MultiWeekMenu with _$MultiWeekMenu {
.add(
IngredientMealRequirement(
weekIndex: fed.weekIndex,
cookWeekIndex: fed.cookWeekIndex,
cookDayIndex: fed.cookDayIndex,
mealTime: fed.mealTime,
subMealIndex: fed.subMealIndex,
recipeId: fed.recipe.id,
Expand Down
17 changes: 12 additions & 5 deletions menu_management/lib/shopping/ingredient_meal_requirement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@ part "ingredient_meal_requirement.g.dart";
/// [isCookEvent] is false when the meal eats leftovers of an earlier cook. Such a meal still
/// needs the ingredient, because the earlier cook buys the food for it.
///
/// [cookWeekIndex] is the week of the cook event that cooks the food of this meal. For a cook
/// meal it equals [weekIndex]. For a leftover meal it can be an earlier week, because a cook
/// event late in one week can feed a meal of the next week. The shopping trip that buys the
/// food is the trip of that cook event, so use [cookWeekIndex] to put the meal under a trip.
/// [cookDayIndex] is the day of the cook event that cooks the food of this meal, counted from the
/// first day of the menu (`weekIndex * 7 + weekDay`, the same count as `CookingEvent.dayIndex`).
/// For a leftover meal it is an earlier day, and it can be in an earlier week, because a cook
/// event late in one week can feed a meal of the next week. The shopping trip that buys the food
/// is the trip that buys that cook day (`TripItem.cookDays`), so use [cookDayIndex] to put the
/// meal under a trip.
///
/// The record holds the recipe id, never the Recipe object, the same way Cooking does.
@freezed
abstract class IngredientMealRequirement with _$IngredientMealRequirement {
const factory IngredientMealRequirement({
required int weekIndex,
required int cookWeekIndex,
required int cookDayIndex,
required MealTime mealTime,
required int subMealIndex,
required String recipeId,
Expand All @@ -34,5 +36,10 @@ abstract class IngredientMealRequirement with _$IngredientMealRequirement {
@Default([]) List<Quantity> quantities,
}) = _IngredientMealRequirement;

const IngredientMealRequirement._();

/// The week of the cook event that cooks the food of this meal.
int get cookWeekIndex => cookDayIndex ~/ 7;

factory IngredientMealRequirement.fromJson(Map<String, Object?> json) => _$IngredientMealRequirementFromJson(json);
}
Loading
Loading