diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 0452e9bfc..6a072be7d 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -50159,6 +50159,7 @@ components: enum: - none - unrated + - in_progress - low - below - usual @@ -50166,12 +50167,14 @@ components: - high description: "Where the day sits in the person's own spread over the 84 days before the period: `low` below their 10th percentile, `below` below the 25th, `above` above the 75th, `high` above the 90th, `usual` - between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with." + between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with; + `in_progress` = today, which has not ended and is never rated." activeMinutes: type: string enum: - none - unrated + - in_progress - low - below - usual @@ -50179,12 +50182,14 @@ components: - high description: "Where the day sits in the person's own spread over the 84 days before the period: `low` below their 10th percentile, `below` below the 25th, `above` above the 75th, `high` above the 90th, `usual` - between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with." + between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with; + `in_progress` = today, which has not ended and is never rated." load: type: string enum: - none - unrated + - in_progress - low - below - usual @@ -50192,7 +50197,8 @@ components: - high description: "Where the day sits in the person's own spread over the 84 days before the period: `low` below their 10th percentile, `below` below the 25th, `above` above the 75th, `high` above the 90th, `usual` - between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with." + between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with; + `in_progress` = today, which has not ended and is never rated." required: - steps - activeMinutes @@ -50412,6 +50418,17 @@ components: type: integer minimum: -9007199254740991 maximum: 9007199254740991 + completedDays: + type: integer + minimum: -9007199254740991 + maximum: 9007199254740991 + description: Days of the period that have ended (today excluded). + completedValue: + description: "`value` over the completed days only. This, not `value`, is what `usualToDate` is compared with, so a + running period is never judged on today's partial day." + type: + - number + - "null" usual: description: "The person's usual full period: the median of the twelve weeks (or, per day, of the six months) before it. Null with fewer than three earlier periods of data." @@ -50419,7 +50436,8 @@ components: - number - "null" usualToDate: - description: The usual figure for the days elapsed so far; equals `usual` once the period is over. + description: The usual figure for the completed days (today excluded); equals `usual` once the period is over, null + before the period's first day has ended. type: - number - "null" @@ -50430,6 +50448,8 @@ components: required: - value - daysWithData + - completedDays + - completedValue - usual - usualToDate - referencePeriods diff --git a/messages/de.json b/messages/de.json index 0bea7d105..e1083aa25 100644 --- a/messages/de.json +++ b/messages/de.json @@ -13640,7 +13640,8 @@ "below": "Unter deinem Üblichen", "usual": "Etwa wie üblich", "above": "Über deinem Üblichen", - "high": "Weit über deinem Üblichen" + "high": "Weit über deinem Üblichen", + "in_progress": "Heute, noch nicht vorbei" }, "legend": "Verglichen mit deinen 12 Wochen davor:", "noBaseline": "Dein üblicher Bereich erscheint nach 14 Tagen mit Daten.", @@ -13650,7 +13651,9 @@ "workouts": "{value} Trainings", "load": "Durchschnittliche Trainingslast {value}", "usual": "Bis hierher üblich bei dir: {value}", - "noUsual": "Noch zu wenige frühere Wochen oder Monate zum Vergleichen." + "noUsual": "Noch zu wenige frühere Wochen oder Monate zum Vergleichen.", + "usualThroughYesterday": "Bis gestern {value}, bis dahin üblich bei dir {usual}", + "firstDay": "Der Vergleich mit deinem Üblichen kommt, sobald der erste Tag vorbei ist." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/en.json b/messages/en.json index 6647f6d01..a90ca8022 100644 --- a/messages/en.json +++ b/messages/en.json @@ -13640,7 +13640,8 @@ "below": "Below your usual", "usual": "About your usual", "above": "Above your usual", - "high": "Far above your usual" + "high": "Far above your usual", + "in_progress": "Today, still in progress" }, "legend": "Compared with your 12 weeks before:", "noBaseline": "Your usual range shows after 14 days with data.", @@ -13650,7 +13651,9 @@ "workouts": "{value} workouts", "load": "Average training load {value}", "usual": "Your usual by this point: {value}", - "noUsual": "Not enough earlier weeks or months to compare with yet." + "noUsual": "Not enough earlier weeks or months to compare with yet.", + "usualThroughYesterday": "Through yesterday {value}, your usual by then {usual}", + "firstDay": "Compared with your usual once the first day is over." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/es.json b/messages/es.json index dd7b71ab8..032dbfaca 100644 --- a/messages/es.json +++ b/messages/es.json @@ -13640,7 +13640,8 @@ "below": "Por debajo de lo habitual", "usual": "Como de costumbre", "above": "Por encima de lo habitual", - "high": "Muy por encima de lo habitual" + "high": "Muy por encima de lo habitual", + "in_progress": "Hoy, aún en curso" }, "legend": "Comparado con tus 12 semanas anteriores:", "noBaseline": "Tu rango habitual aparece tras 14 días con datos.", @@ -13650,7 +13651,9 @@ "workouts": "{value} entrenamientos", "load": "Carga de entrenamiento media {value}", "usual": "Lo habitual hasta este punto: {value}", - "noUsual": "Aún no hay suficientes semanas o meses anteriores para comparar." + "noUsual": "Aún no hay suficientes semanas o meses anteriores para comparar.", + "usualThroughYesterday": "Hasta ayer {value}, lo habitual para ti hasta entonces {usual}", + "firstDay": "Se compara con lo habitual cuando termine el primer día." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/fr.json b/messages/fr.json index 9f408bbe0..c41d73f87 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -13640,7 +13640,8 @@ "below": "En dessous de ton habitude", "usual": "Comme d'habitude", "above": "Au-dessus de ton habitude", - "high": "Bien au-dessus de ton habitude" + "high": "Bien au-dessus de ton habitude", + "in_progress": "Aujourd’hui, en cours" }, "legend": "Comparé à tes 12 semaines précédentes :", "noBaseline": "Ta plage habituelle apparaît après 14 jours avec des données.", @@ -13650,7 +13651,9 @@ "workouts": "{value} entraînements", "load": "Charge d'entraînement moyenne {value}", "usual": "Ton habitude à ce stade : {value}", - "noUsual": "Pas encore assez de semaines ou de mois précédents pour comparer." + "noUsual": "Pas encore assez de semaines ou de mois précédents pour comparer.", + "usualThroughYesterday": "Jusqu’à hier {value}, ton habitude à ce stade {usual}", + "firstDay": "Comparé à ton habitude dès que le premier jour est terminé." }, "sportCount": "{sport} : {count}", "goal": { diff --git a/messages/it.json b/messages/it.json index 140376896..fa3fd0fac 100644 --- a/messages/it.json +++ b/messages/it.json @@ -13640,7 +13640,8 @@ "below": "Sotto il tuo solito", "usual": "Come al solito", "above": "Sopra il tuo solito", - "high": "Molto sopra il tuo solito" + "high": "Molto sopra il tuo solito", + "in_progress": "Oggi, ancora in corso" }, "legend": "Rispetto alle tue 12 settimane precedenti:", "noBaseline": "Il tuo intervallo abituale compare dopo 14 giorni con dati.", @@ -13650,7 +13651,9 @@ "workouts": "{value} allenamenti", "load": "Carico di allenamento medio {value}", "usual": "Il tuo solito fino a questo punto: {value}", - "noUsual": "Non ci sono ancora abbastanza settimane o mesi precedenti per un confronto." + "noUsual": "Non ci sono ancora abbastanza settimane o mesi precedenti per un confronto.", + "usualThroughYesterday": "Fino a ieri {value}, il tuo solito fino a quel punto {usual}", + "firstDay": "Il confronto con il tuo solito arriva quando il primo giorno è finito." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/ko.json b/messages/ko.json index b29dd3b6c..0777b5263 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -13640,7 +13640,8 @@ "below": "평소보다 적음", "usual": "평소와 비슷함", "above": "평소보다 많음", - "high": "평소보다 훨씬 많음" + "high": "평소보다 훨씬 많음", + "in_progress": "오늘, 아직 진행 중" }, "legend": "이전 12주와 비교:", "noBaseline": "데이터가 있는 날이 14일이 되면 평소 범위가 표시됩니다.", @@ -13650,7 +13651,9 @@ "workouts": "운동 {value}회", "load": "평균 운동 부하 {value}", "usual": "이 시점까지의 평소 값: {value}", - "noUsual": "비교할 이전 주나 달이 아직 충분하지 않습니다." + "noUsual": "비교할 이전 주나 달이 아직 충분하지 않습니다.", + "usualThroughYesterday": "어제까지 {value}, 그때까지의 평소 {usual}", + "firstDay": "첫날이 끝나면 평소와 비교합니다." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/nl.json b/messages/nl.json index ebef8b7d0..ecd903c04 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -13640,7 +13640,8 @@ "below": "Onder je gewone niveau", "usual": "Ongeveer zoals gewoonlijk", "above": "Boven je gewone niveau", - "high": "Ver boven je gewone niveau" + "high": "Ver boven je gewone niveau", + "in_progress": "Vandaag, nog bezig" }, "legend": "Vergeleken met je 12 weken daarvoor:", "noBaseline": "Je gebruikelijke bereik verschijnt na 14 dagen met gegevens.", @@ -13650,7 +13651,9 @@ "workouts": "{value} trainingen", "load": "Gemiddelde trainingsbelasting {value}", "usual": "Gebruikelijk tot dit punt: {value}", - "noUsual": "Nog niet genoeg eerdere weken of maanden om mee te vergelijken." + "noUsual": "Nog niet genoeg eerdere weken of maanden om mee te vergelijken.", + "usualThroughYesterday": "Tot en met gisteren {value}, je gebruikelijke tot dan {usual}", + "firstDay": "Vergeleken met je gebruikelijke zodra de eerste dag voorbij is." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/messages/pl.json b/messages/pl.json index 6a641519c..fd13d2ec0 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -13640,7 +13640,8 @@ "below": "Poniżej twojej normy", "usual": "Mniej więcej jak zwykle", "above": "Powyżej twojej normy", - "high": "Znacznie powyżej twojej normy" + "high": "Znacznie powyżej twojej normy", + "in_progress": "Dziś, jeszcze trwa" }, "legend": "W porównaniu z twoimi 12 wcześniejszymi tygodniami:", "noBaseline": "Twój zwykły zakres pojawi się po 14 dniach z danymi.", @@ -13650,7 +13651,9 @@ "workouts": "Treningi: {value}", "load": "Średnie obciążenie treningowe {value}", "usual": "Twoja norma do tego momentu: {value}", - "noUsual": "Za mało wcześniejszych tygodni lub miesięcy do porównania." + "noUsual": "Za mało wcześniejszych tygodni lub miesięcy do porównania.", + "usualThroughYesterday": "Do wczoraj {value}, twoja zwykła wartość do tego dnia {usual}", + "firstDay": "Porównanie ze zwykłą wartością pojawi się po zakończeniu pierwszego dnia." }, "sportCount": "{sport}: {count}", "goal": { diff --git a/src/components/activity/__tests__/activity-calendar-card.test.tsx b/src/components/activity/__tests__/activity-calendar-card.test.tsx index a005c664f..d9c991492 100644 --- a/src/components/activity/__tests__/activity-calendar-card.test.tsx +++ b/src/components/activity/__tests__/activity-calendar-card.test.tsx @@ -58,6 +58,16 @@ function calendar() { load: null, }); } + // This morning: a few hundred steps so far. + input.push({ + date: today, + steps: 400, + exerciseMinutes: null, + workoutMinutes: 0, + workoutCount: 0, + workoutsBySport: {}, + load: null, + }); input.push({ date: "2026-10-05", steps: 20000, @@ -113,10 +123,19 @@ describe("ActivityCalendarCard", () => { state.data = calendar(); const html = render(); expect(html).toContain('data-metric="steps"'); - expect(html).toContain("Your usual by this point:"); + // A running month is compared through yesterday, not on today. + expect(html).toContain("Through yesterday"); + expect(html).toContain("your usual by then"); expect(html).toContain("Running: 1"); }); + it("marks today as in progress rather than rating it", () => { + state.data = calendar(); + const html = render(); + expect(html).toMatch(/data-date="2026-10-21" data-level="in_progress"/); + expect(html).toContain("Today, still in progress"); + }); + it("shows goal progress and a week tick column", () => { state.data = calendar(); const html = render(); diff --git a/src/components/activity/activity-calendar-card.tsx b/src/components/activity/activity-calendar-card.tsx index 7e9eef083..f941dcae2 100644 --- a/src/components/activity/activity-calendar-card.tsx +++ b/src/components/activity/activity-calendar-card.tsx @@ -41,6 +41,7 @@ type DayLevel = CalendarDay["level"]["steps"]; const LEVEL_BAR: Record = { none: "bg-transparent", unrated: "bg-muted-foreground/60", + in_progress: "bg-muted-foreground/30", low: "bg-chart-2/20", below: "bg-chart-2/40", usual: "bg-chart-2/60", @@ -412,10 +413,13 @@ export function ActivityCalendarCard() { .filter((m) => data.available[m]) .map((m) => { const s = data.summary[m]; + // Compared over completed days only: today has not ended. const delta = - s.value !== null && s.usualToDate - ? (s.value - s.usualToDate) / s.usualToDate + s.completedValue !== null && s.usualToDate + ? (s.completedValue - s.usualToDate) / s.usualToDate : null; + const running = + data.today >= data.start && data.today <= data.end; return (
  • - {s.usualToDate === null - ? t("activityCalendar.summary.noUsual") - : t("activityCalendar.summary.usual", { - value: formatMetric(m, s.usualToDate), - })} + {s.usual !== null && s.completedDays === 0 + ? t("activityCalendar.summary.firstDay") + : s.usualToDate === null + ? t("activityCalendar.summary.noUsual") + : running + ? t( + "activityCalendar.summary.usualThroughYesterday", + { + value: + s.completedValue === null + ? "–" + : formatMetric(m, s.completedValue), + usual: formatMetric(m, s.usualToDate), + }, + ) + : t("activityCalendar.summary.usual", { + value: formatMetric(m, s.usualToDate), + })}

    {delta !== null ? ( diff --git a/src/lib/activity/__tests__/calendar-math.test.ts b/src/lib/activity/__tests__/calendar-math.test.ts index 816cec6ab..1967237af 100644 --- a/src/lib/activity/__tests__/calendar-math.test.ts +++ b/src/lib/activity/__tests__/calendar-math.test.ts @@ -143,7 +143,7 @@ describe("period against the usual period", () => { expect(result.referencePeriods).toBe(12); }); - it("pro-rates the usual for a period still running", () => { + it("compares a running period over its completed days, never today's partial day", () => { const bounds = periodBounds("week", "2026-10-21"); const values = new Map(); for (let i = 1; i <= 84; i++) @@ -155,9 +155,27 @@ describe("period against the usual period", () => { values.get(d) ?? (d <= "2026-10-21" && d >= bounds.start ? 2000 : null), mode: "sum", }); + // The total shown counts today; the comparison stops at yesterday. expect(result.value).toBe(6000); expect(result.usual).toBe(7000); - expect(result.usualToDate).toBe(3000); + expect(result.completedDays).toBe(2); + expect(result.completedValue).toBe(4000); + expect(result.usualToDate).toBe(2000); + }); + + it("has nothing to compare on the first day of a period", () => { + const bounds = periodBounds("week", "2026-10-19"); + const result = comparePeriod({ + bounds, + today: "2026-10-19", // Monday + valueOf: (d) => (d <= "2026-10-19" ? 1000 : null), + mode: "sum", + }); + expect(result.value).toBe(1000); + expect(result.usual).toBe(7000); + expect(result.completedDays).toBe(0); + expect(result.completedValue).toBeNull(); + expect(result.usualToDate).toBeNull(); }); it("compares months per day, so February meets March fairly", () => { @@ -258,6 +276,28 @@ describe("the assembled calendar", () => { ).toBe("none"); }); + it("shows today as in progress, never rated against whole days", () => { + const morning = buildActivityCalendar({ + bounds, + today, + input: [...input, day(today, { steps: 800 })], + goals: NO_ACTIVITY_GOALS, + }); + const todayCell = morning.days.find((d) => d.date === today); + expect(todayCell?.steps).toBe(800); + expect(todayCell?.level.steps).toBe("in_progress"); + expect(todayCell?.level.activeMinutes).toBe("in_progress"); + expect(todayCell?.level.load).toBe("none"); + // Yesterday, with the same value, is rated. + const yesterday = buildActivityCalendar({ + bounds, + today, + input: [...input, day("2026-10-20", { steps: 800 })], + goals: NO_ACTIVITY_GOALS, + }).days.find((d) => d.date === "2026-10-20"); + expect(yesterday?.level.steps).toBe("low"); + }); + it("counts workouts by sport in the period", () => { expect(calendar.sports).toEqual([ { sportType: "cycling", count: 1 }, diff --git a/src/lib/activity/calendar-math.ts b/src/lib/activity/calendar-math.ts index 6f4a5e225..6a6f821ab 100644 --- a/src/lib/activity/calendar-math.ts +++ b/src/lib/activity/calendar-math.ts @@ -11,14 +11,17 @@ * above the 90th `high`, everything between `usual`. With fewer than * `MIN_BASELINE_DAYS` days that carry a value the day is `unrated`: * there is not enough history to say what usual is, and the calendar says - * so instead of guessing. + * so instead of guessing. TODAY is `in_progress`, never rated: a day + * that has not ended would read as a low day every morning. * - A PERIOD total is placed against the median total of the same kind of * period before it: the twelve weeks before a week, the six months before * a month. A month is compared per day (months differ in length), so the * usual month is the median daily rate of the earlier months times this - * month's length. A period still running is compared with the usual rate - * times the days elapsed so far, not with a full period it cannot have - * reached yet. At least `MIN_REFERENCE_PERIODS` earlier periods with data + * month's length. A period still running is compared over its COMPLETED + * days only: the total through yesterday against the usual rate times the + * days completed, never a full period it cannot have reached yet, and + * never today's partial day (a morning would always read as a shortfall). + * The period total shown and the goals still count today. At least `MIN_REFERENCE_PERIODS` earlier periods with data * are needed; fewer gives no usual. * * What a day's value is: @@ -62,7 +65,14 @@ export const MIN_REFERENCE_PERIODS = 3; export type ActivityPeriod = "week" | "month"; export type RatedMetric = "steps" | "activeMinutes" | "load"; export type DayLevel = - "none" | "unrated" | "low" | "below" | "usual" | "above" | "high"; + | "none" + | "unrated" + | "in_progress" + | "low" + | "below" + | "usual" + | "above" + | "high"; /** One local day of raw signals, as the reader assembles it. */ export interface ActivityDayInput { @@ -237,13 +247,17 @@ export function dayRange(from: string, to: string): string[] { } export interface PeriodComparison { - /** The period's total (steps, active minutes, workouts) or mean (load), over the days so far. */ + /** The period's total (steps, active minutes, workouts) or mean (load), over the days so far, today included. */ value: number | null; /** Days of the period that carry a value. */ daysWithData: number; + /** Days of the period that have ended (today excluded). */ + completedDays: number; + /** `value` over the completed days only: what `usualToDate` is compared with. */ + completedValue: number | null; /** The usual full period, or null when there is not enough history. */ usual: number | null; - /** The usual figure for the days elapsed so far (equals `usual` for a finished period). */ + /** The usual figure for the completed days (equals `usual` for a finished period); null before the first day has ended. */ usualToDate: number | null; /** Earlier periods the usual was computed from. */ referencePeriods: number; @@ -293,15 +307,21 @@ export function comparePeriod(input: { }): PeriodComparison { const { bounds, today, valueOf, mode } = input; const lastCounted = bounds.end < today ? bounds.end : today; + const yesterday = shiftDateKey(today, -1); + const lastCompleted = bounds.end < yesterday ? bounds.end : yesterday; const length = daysBetweenDateKeys(bounds.start, bounds.end) + 1; - const elapsed = - lastCounted < bounds.start - ? 0 - : daysBetweenDateKeys(bounds.start, lastCounted) + 1; + const daysUpTo = (last: string) => + last < bounds.start ? 0 : daysBetweenDateKeys(bounds.start, last) + 1; + const elapsed = daysUpTo(lastCounted); + const completed = daysUpTo(lastCompleted); const current = elapsed === 0 ? { value: null, n: 0 } : aggregate(dayRange(bounds.start, lastCounted).map(valueOf), mode); + const done = + completed === 0 + ? { value: null, n: 0 } + : aggregate(dayRange(bounds.start, lastCompleted).map(valueOf), mode); // Each earlier period becomes a per-day rate (sum) or a mean (mean), so a // 28-day February and a 31-day March compare on the same footing. @@ -319,13 +339,15 @@ export function comparePeriod(input: { usualRate === null ? null : mode === "sum" - ? usualRate * elapsed + ? usualRate * completed : usualRate; return { value: current.value, daysWithData: current.n, + completedDays: completed, + completedValue: done.value, usual, - usualToDate: elapsed === 0 ? null : usualToDate, + usualToDate: completed === 0 ? null : usualToDate, referencePeriods: rates.length, }; } @@ -477,6 +499,14 @@ export function buildActivityCalendar(args: { ? null : activeMinutesOf(day, hasActivityOn(date)); const load = future ? null : day.load; + // Today has not ended: shown, never rated against whole days. + const levelOf = ( + value: number | null, + stats: BaselineStats | null, + ): DayLevel => + date === today && value !== null + ? "in_progress" + : dayLevel(value, stats); return { date, inPeriod: date >= bounds.start && date <= bounds.end, @@ -487,9 +517,9 @@ export function buildActivityCalendar(args: { workoutsBySport: future ? {} : day.workoutsBySport, load, level: { - steps: dayLevel(steps, baseline.steps), - activeMinutes: dayLevel(activeMinutes, baseline.activeMinutes), - load: dayLevel(load, baseline.load), + steps: levelOf(steps, baseline.steps), + activeMinutes: levelOf(activeMinutes, baseline.activeMinutes), + load: levelOf(load, baseline.load), }, }; }, diff --git a/src/lib/validations/activity-calendar.ts b/src/lib/validations/activity-calendar.ts index 75ba90c65..0aa7d8155 100644 --- a/src/lib/validations/activity-calendar.ts +++ b/src/lib/validations/activity-calendar.ts @@ -21,9 +21,18 @@ export type ActivityCalendarQuery = z.infer; const dateKey = z.iso.date(); const dayLevelEnum = z - .enum(["none", "unrated", "low", "below", "usual", "above", "high"]) + .enum([ + "none", + "unrated", + "in_progress", + "low", + "below", + "usual", + "above", + "high", + ]) .describe( - "Where the day sits in the person's own spread over the 84 days before the period: `low` below their 10th percentile, `below` below the 25th, `above` above the 75th, `high` above the 90th, `usual` between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with.", + "Where the day sits in the person's own spread over the 84 days before the period: `low` below their 10th percentile, `below` below the 25th, `above` above the 75th, `high` above the 90th, `usual` between. `none` = no value that day; `unrated` = fewer than 14 days of history to compare with; `in_progress` = today, which has not ended and is never rated.", ); const baselineStatsSchema = z @@ -50,6 +59,16 @@ const periodComparisonSchema = z "The period so far: a total (steps, active minutes, workouts) or a mean (load).", ), daysWithData: z.number().int(), + completedDays: z + .number() + .int() + .describe("Days of the period that have ended (today excluded)."), + completedValue: z + .number() + .nullable() + .describe( + "`value` over the completed days only. This, not `value`, is what `usualToDate` is compared with, so a running period is never judged on today's partial day.", + ), usual: z .number() .nullable() @@ -60,7 +79,7 @@ const periodComparisonSchema = z .number() .nullable() .describe( - "The usual figure for the days elapsed so far; equals `usual` once the period is over.", + "The usual figure for the completed days (today excluded); equals `usual` once the period is over, null before the period's first day has ended.", ), referencePeriods: z.number().int(), })