mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Keep the rolling anchor when a start only moved forward
An intake before startsOn stopped anchoring a rolling cadence, which also moved the next dose of a single-course medication whose start had merely been pushed forward a few days. The intake is now ignored only when the dose it implies would fall before the course starts (a new course after a gap); otherwise it anchors as before.
This commit is contained in:
1 parent
eada82f61e
commit
2c2151ae70
3 files changed
+53
-22
No files matched your search
@@ -158,15 +158,18 @@ export function buildCurrentCycle(
|
||||
if (canonical.scheduleType === "PRN") continue;
|
||||
const n = canonical.rollingIntervalDays;
|
||||
if (n !== null && n > 0) {
|
||||
// v1.40 (#1024) — an intake from a previous course does not anchor the
|
||||
// current one; its first dose is due on its start day.
|
||||
const intakeInCourse =
|
||||
lastIntakeAt !== null &&
|
||||
(ctx.startsOn === null ||
|
||||
userDayKey(lastIntakeAt, ctx.timeZone) >= dayKeyOfDate(ctx.startsOn));
|
||||
const anchor =
|
||||
intakeInCourse && lastIntakeAt !== null
|
||||
// v1.40 (#1024) — an intake whose next dose would fall before the
|
||||
// course starts belongs to an earlier course and does not anchor this
|
||||
// one (the recurrence engine's rule); otherwise it anchors as before.
|
||||
const fromIntake =
|
||||
lastIntakeAt !== null
|
||||
? new Date(lastIntakeAt.getTime() + n * DAY_MS)
|
||||
: null;
|
||||
const anchor =
|
||||
fromIntake !== null &&
|
||||
(ctx.startsOn === null ||
|
||||
userDayKey(fromIntake, ctx.timeZone) >= dayKeyOfDate(ctx.startsOn))
|
||||
? fromIntake
|
||||
: (ctx.startsOn ?? ctx.createdAt);
|
||||
if (ctx.endsOn && anchor.getTime() > ctx.endsOn.getTime()) continue;
|
||||
const graceMs = (canonical.reminderGraceMinutes ?? 60) * 60 * 1000;
|
||||
|
||||
@@ -242,6 +242,29 @@ describe("occurrencesBetween — RRULE MONTHLY / YEARLY", () => {
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("occurrencesBetween — rolling", () => {
|
||||
it("keeps anchoring on an intake when the start was only moved forward a little (v1.40)", () => {
|
||||
// Weekly. Last dose Jun 8; the start was then moved to Jun 10. The next
|
||||
// dose stays Jun 15 (last dose + 7), as it was before courses existed.
|
||||
const NOW = d("2026-06-11T12:00:00Z");
|
||||
const schedule = makeSchedule({
|
||||
rollingIntervalDays: 7,
|
||||
timesOfDay: ["08:00"],
|
||||
});
|
||||
const ctx = makeCtx({
|
||||
lastIntakeAt: d("2026-06-08T06:00:00Z"),
|
||||
medication: { startsOn: d("2026-06-10T00:00:00Z") },
|
||||
});
|
||||
const slots = occurrencesBetween(
|
||||
schedule,
|
||||
NOW,
|
||||
new Date(NOW.getTime() + 14 * 24 * 60 * 60 * 1000),
|
||||
ctx,
|
||||
);
|
||||
expect(slots.map((o) => o.at.toISOString())).toEqual([
|
||||
"2026-06-15T06:00:00.000Z",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not let an intake from a previous course anchor a new one (v1.40, #1024)", () => {
|
||||
// The previous course's last dose was months ago; the new course starts
|
||||
// in two days. Its first dose is due on its start day, not on
|
||||
|
||||
@@ -493,23 +493,28 @@ function rollingDoseAnchor(
|
||||
ctx: RecurrenceContext,
|
||||
): Date {
|
||||
const intervalDays = schedule.rollingIntervalDays ?? 0;
|
||||
// v1.40 (#1024) — an intake from before the course started (a previous
|
||||
// course of the same medication) does not anchor this one: the new
|
||||
// course's first dose is due on its start day, as for a course with no
|
||||
// intake at all.
|
||||
const intakeDay =
|
||||
ctx.lastIntakeAt !== null
|
||||
? civilDayOfInstant(ctx.lastIntakeAt, ctx.timeZone)
|
||||
: null;
|
||||
const anchorsThisCourse =
|
||||
intakeDay !== null &&
|
||||
(ctx.medication.startsOn === null ||
|
||||
intakeDay.getTime() >= civilDayOfDate(ctx.medication.startsOn).getTime());
|
||||
// N calendar days after the local day of the last intake, not N × 24 h:
|
||||
// a late-evening dose across a DST change would otherwise slide onto the
|
||||
// wrong day.
|
||||
return anchorsThisCourse
|
||||
? addCivilDays(intakeDay, intervalDays)
|
||||
const fromIntake =
|
||||
ctx.lastIntakeAt !== null
|
||||
? addCivilDays(
|
||||
civilDayOfInstant(ctx.lastIntakeAt, ctx.timeZone),
|
||||
intervalDays,
|
||||
)
|
||||
: null;
|
||||
// v1.40 (#1024) — an intake whose next dose would fall before the course
|
||||
// starts belongs to an earlier course (a new course after a gap): it does
|
||||
// not anchor this one, whose first dose is due on its start day. An intake
|
||||
// whose next dose falls on or after the start (a start moved forward by a
|
||||
// few days) anchors exactly as before.
|
||||
const startDay =
|
||||
ctx.medication.startsOn !== null
|
||||
? civilDayOfDate(ctx.medication.startsOn)
|
||||
: null;
|
||||
return fromIntake !== null &&
|
||||
(startDay === null || fromIntake.getTime() >= startDay.getTime())
|
||||
? fromIntake
|
||||
: anchorCivilDay(ctx);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user