functional

This commit is contained in:
2026-09-17 01:19:19 -04:00
commit 3d4d75fdfb
19 changed files with 3257 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
import type { Config, EstimationResult, MissingPeriod, Plan, Selections } from "./types";
import { clampDate, daysBetween, inclusiveDayCount, overlapDays } from "./dates";
/** The active missing periods for a set of ids, in config order. */
function activePeriods(config: Config, ids: Set<string>): MissingPeriod[] {
return config.missingPeriods.filter((p) => ids.has(p.id));
}
/**
* Days a student is expected to be on campus across the full meal window,
* after removing days covered by the active missing periods.
* Overlapping/adjacent periods are handled by summing per-period overlap with
* the window; periods in the config are assumed non-overlapping (typical for
* distinct breaks). Result is clamped to >= 0.
*/
export function totalActiveDays(config: Config, activeIds: Set<string>): number {
const { mealStartDate, mealEndDate } = config;
const windowDays = inclusiveDayCount(mealStartDate, mealEndDate);
let excluded = 0;
for (const p of activePeriods(config, activeIds)) {
excluded += overlapDays(mealStartDate, mealEndDate, p.startDate, p.endDate);
}
return Math.max(0, windowDays - excluded);
}
/**
* Active days from meal start up to (and including) the target date, minus
* active missing periods within that window, minus extra excluded days.
* The target date is clamped into the meal window first. Result clamped >= 0.
*/
export function activeDaysConsumed(config: Config, selections: Selections): number {
const { mealStartDate, mealEndDate } = config;
// Before the meal period even begins, nothing is consumed yet.
if (daysBetween(mealStartDate, selections.targetDate) < 0) {
return 0;
}
const target = clampDate(selections.targetDate, mealStartDate, mealEndDate);
const windowDays = inclusiveDayCount(mealStartDate, target);
let excluded = 0;
for (const p of activePeriods(config, selections.activeMissingPeriodIds)) {
excluded += overlapDays(mealStartDate, target, p.startDate, p.endDate);
}
const consumed = windowDays - excluded - Math.max(0, selections.extraExcludedDays);
return Math.max(0, consumed);
}
function findPlan(config: Config, planId: string): Plan | undefined {
return config.plans.find((p) => p.id === planId);
}
/**
* Master estimate. Distributes a plan's totals evenly across total active days,
* then reports per-week rate, consumed-by-target, and remaining for both meal
* blocks and FLEX dollars. All output values are clamped to sane bounds.
*/
export function estimate(config: Config, selections: Selections): EstimationResult {
const plan = findPlan(config, selections.selectedPlanId) ?? config.plans[0];
const total = totalActiveDays(config, selections.activeMissingPeriodIds);
const consumedDays = Math.min(
activeDaysConsumed(config, selections),
total
);
// Guard against division by zero when every day is excluded.
const blockRate = total > 0 ? plan.totalMealBlocks / total : 0;
const flexRate = total > 0 ? plan.totalFlexDollars / total : 0;
const consumedMealBlocks = clamp(blockRate * consumedDays, 0, plan.totalMealBlocks);
const consumedFlexDollars = clamp(flexRate * consumedDays, 0, plan.totalFlexDollars);
return {
perWeekMealBlocks: blockRate * 7,
perWeekFlexDollars: flexRate * 7,
consumedMealBlocks,
consumedFlexDollars,
remainingMealBlocks: plan.totalMealBlocks - consumedMealBlocks,
remainingFlexDollars: plan.totalFlexDollars - consumedFlexDollars,
totalActiveDays: total,
activeDaysConsumed: consumedDays,
};
}
function clamp(v: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, v));
}