mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(medications): keep the rrule anchor for a rule with BYSETPOS
The library drops a period's candidates before DTSTART before BYSETPOS picks among them, so a start moved into the middle of a week chose a different day. The property now generates BYSETPOS rules and kept red without the guard.
This commit is contained in:
1 parent
b975e86b39
commit
55a3a9b53d
2 files changed
+39
-23
No files matched your search
@@ -9,7 +9,10 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { alignedRruleStart } from "@/lib/medications/scheduling/aligned-rrule-start";
|
||||
|
||||
import { assertProperty } from "../../../../../tests/property/fast-check";
|
||||
import {
|
||||
assertProperty,
|
||||
propertyTimeout,
|
||||
} from "../../../../../tests/property/fast-check";
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const BYDAY = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] as const;
|
||||
@@ -33,8 +36,9 @@ const ruleArb = fc
|
||||
),
|
||||
wkst: fc.option(fc.constantFrom("MO", "SU")),
|
||||
count: fc.option(fc.integer({ min: 1, max: 40 })),
|
||||
bysetpos: fc.option(fc.constantFrom(1, 2, -1)),
|
||||
})
|
||||
.map(({ freq, interval, byday, byhour, wkst, count }) =>
|
||||
.map(({ freq, interval, byday, byhour, wkst, count, bysetpos }) =>
|
||||
[
|
||||
`FREQ=${freq}`,
|
||||
interval > 1 ? `INTERVAL=${interval}` : null,
|
||||
@@ -42,6 +46,7 @@ const ruleArb = fc
|
||||
byhour ? `BYHOUR=${byhour.join(",")}` : null,
|
||||
wkst ? `WKST=${wkst}` : null,
|
||||
count ? `COUNT=${count}` : null,
|
||||
bysetpos && byday && !byhour ? `BYSETPOS=${bysetpos}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(";"),
|
||||
@@ -61,26 +66,32 @@ function occurrences(rule: string, start: Date, after: Date, before: Date) {
|
||||
}
|
||||
|
||||
describe("alignedRruleStart", () => {
|
||||
it("leaves every occurrence in the window where it was", () => {
|
||||
assertProperty(
|
||||
fc.property(
|
||||
ruleArb,
|
||||
dayArb,
|
||||
fc.integer({ min: -30, max: 2 * 365 }),
|
||||
fc.integer({ min: 1, max: 120 }),
|
||||
(rule, anchor, offsetDays, spanDays) => {
|
||||
const after = new Date(anchor.getTime() + offsetDays * DAY_MS);
|
||||
const before = new Date(after.getTime() + spanDays * DAY_MS);
|
||||
const aligned = alignedRruleStart(rule, anchor, after);
|
||||
expect(aligned.getTime()).toBeGreaterThanOrEqual(anchor.getTime());
|
||||
expect(occurrences(rule, aligned, after, before)).toEqual(
|
||||
occurrences(rule, anchor, after, before),
|
||||
);
|
||||
},
|
||||
),
|
||||
300,
|
||||
);
|
||||
});
|
||||
it(
|
||||
"leaves every occurrence in the window where it was",
|
||||
() => {
|
||||
assertProperty(
|
||||
fc.property(
|
||||
ruleArb,
|
||||
dayArb,
|
||||
fc.integer({ min: -30, max: 2 * 365 }),
|
||||
fc.integer({ min: 1, max: 120 }),
|
||||
(rule, anchor, offsetDays, spanDays) => {
|
||||
const after = new Date(anchor.getTime() + offsetDays * DAY_MS);
|
||||
const before = new Date(after.getTime() + spanDays * DAY_MS);
|
||||
const aligned = alignedRruleStart(rule, anchor, after);
|
||||
expect(aligned.getTime()).toBeGreaterThanOrEqual(anchor.getTime());
|
||||
expect(occurrences(rule, aligned, after, before)).toEqual(
|
||||
occurrences(rule, anchor, after, before),
|
||||
);
|
||||
},
|
||||
),
|
||||
300,
|
||||
);
|
||||
// The reference walks from the real anchor, which is the cost this
|
||||
// module removes from the engine.
|
||||
},
|
||||
propertyTimeout(30_000),
|
||||
);
|
||||
|
||||
it("moves a daily or weekly start close to the window", () => {
|
||||
const anchor = new Date(Date.UTC(2024, 5, 1));
|
||||
|
||||
@@ -14,7 +14,8 @@ const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
* occurrence inside it where it was: the weekday, the week parity of an
|
||||
* every-other-week rule and the BYHOUR expansion all ride on the period.
|
||||
* A rule with COUNT keeps its anchor (the count runs from the first
|
||||
* occurrence), and so does any other frequency, whose period is not a fixed
|
||||
* occurrence), as does one with BYSETPOS (see below), and so does any other
|
||||
* frequency, whose period is not a fixed
|
||||
* number of days. The day before `after` is the latest safe start, because
|
||||
* a start the rule does not match is not an occurrence, and anything
|
||||
* between it and `after` is outside the window anyway.
|
||||
@@ -31,6 +32,10 @@ export function alignedRruleStart(
|
||||
return anchor;
|
||||
}
|
||||
if (options.count != null) return anchor;
|
||||
// BYSETPOS picks among a period's candidates, and the library drops the
|
||||
// candidates before DTSTART in the first period before it picks, so a
|
||||
// start moved into the middle of a week would choose a different day.
|
||||
if (options.bysetpos != null) return anchor;
|
||||
const interval = options.interval ?? 1;
|
||||
if (!Number.isInteger(interval) || interval < 1) return anchor;
|
||||
const periodDays =
|
||||
|
||||
Reference in new issue
Block a user