mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(medications): count every weekday a monthly rule names in the supply runway
FREQ=MONTHLY;BYDAY=MO means every Monday of the month, but the runway counted it as one dose a month, so a Monday medication's supply read about four times longer than it lasts and its low-supply notice came late. A weekday without an ordinal now counts as every such day in the month (yearly rules per BYMONTH month), a weekday with one (1MO, -1FR) as one day, and BYSETPOS as the number of positions it picks.
This commit is contained in:
1 parent
612eedcbbb
commit
4d3a98ed43
2 files changed
+60
-5
No files matched your search
@@ -394,6 +394,26 @@ describe("estimateDailyDoseCount — cadence parts the rate used to drop", () =>
|
||||
).toBeCloseTo(2 / 30, 10);
|
||||
});
|
||||
|
||||
it.each([
|
||||
// Every Monday of the month is a weekly dose, not one a month.
|
||||
["FREQ=MONTHLY;BYDAY=MO", 1 / 7],
|
||||
["FREQ=MONTHLY;BYDAY=MO,TH", 2 / 7],
|
||||
// With an ordinal it is one day of the month.
|
||||
["FREQ=MONTHLY;BYDAY=1MO", 1 / 30],
|
||||
["FREQ=MONTHLY;BYDAY=1MO,-1FR", 2 / 30],
|
||||
// The last weekday of the month: BYSETPOS picks one of the five.
|
||||
["FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", 1 / 30],
|
||||
["FREQ=MONTHLY;INTERVAL=2;BYDAY=MO", 1 / 14],
|
||||
// Every Monday in January, and the first Monday of March and September.
|
||||
["FREQ=YEARLY;BYMONTH=1;BYDAY=MO", 30 / 7 / 365],
|
||||
["FREQ=YEARLY;BYMONTH=3,9;BYDAY=1MO", 2 / 365],
|
||||
])("counts the occurrences of %s", (rrule, perDay) => {
|
||||
expect(estimateDailyDoseCount([schedule({ rrule })])).toBeCloseTo(
|
||||
perDay,
|
||||
10,
|
||||
);
|
||||
});
|
||||
|
||||
it("stretches a daily RRULE by its INTERVAL (every other day)", () => {
|
||||
expect(
|
||||
estimateDailyDoseCount([schedule({ rrule: "FREQ=DAILY;INTERVAL=2" })]),
|
||||
|
||||
@@ -51,6 +51,38 @@ function rruleListCount(rrule: string, key: string): number {
|
||||
return m ? m[1].split(",").filter((v) => v.trim() !== "").length : 0;
|
||||
}
|
||||
|
||||
/** The raw values of an RRULE list part (`BYDAY=1MO,FR` → `1MO`, `FR`). */
|
||||
function rruleList(rrule: string, key: string): string[] {
|
||||
const m = new RegExp(`(?:^|;)${key}=([^;]+)`, "i").exec(rrule);
|
||||
return m ? m[1].split(",").filter((v) => v.trim() !== "") : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Occurrences one period of a monthly or yearly rule holds, with the period
|
||||
* `periodDays` long. `BYSETPOS` picks that many of the candidates. A
|
||||
* `BYDAY` entry with an ordinal (`1MO`, `-1FR`) is one day of the period;
|
||||
* one without (`MO`) is every such weekday in it, about `periodDays / 7`.
|
||||
* Counting `FREQ=MONTHLY;BYDAY=MO` as one dose a month made a Monday
|
||||
* medication's supply read four times longer than it lasts.
|
||||
*/
|
||||
function occurrencesPerPeriod(
|
||||
rrule: string,
|
||||
periodDays: number,
|
||||
fallback: number,
|
||||
): number {
|
||||
const setPos = rruleListCount(rrule, "BYSETPOS");
|
||||
if (setPos > 0) return setPos;
|
||||
const monthDays = rruleListCount(rrule, "BYMONTHDAY");
|
||||
if (monthDays > 0) return monthDays;
|
||||
const byDay = rruleList(rrule, "BYDAY");
|
||||
if (byDay.length === 0) return fallback;
|
||||
let count = 0;
|
||||
for (const entry of byDay) {
|
||||
count += /^[+-]?\d/.test(entry.trim()) ? 1 : periodDays / 7;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate doses per day for ONE schedule: times-of-day count, scaled
|
||||
* down by the cadence (rolling interval, weekly day picks + interval
|
||||
@@ -82,14 +114,17 @@ function cadenceDailyDoseCount(s: RunwaySchedule): number {
|
||||
return times / interval;
|
||||
}
|
||||
if (/FREQ=MONTHLY/i.test(rrule)) {
|
||||
const perMonth = Math.max(
|
||||
1,
|
||||
rruleListCount(rrule, "BYMONTHDAY") || rruleListCount(rrule, "BYDAY"),
|
||||
);
|
||||
const perMonth = occurrencesPerPeriod(rrule, 30, 1);
|
||||
return (times * perMonth) / (30 * interval);
|
||||
}
|
||||
if (/FREQ=YEARLY/i.test(rrule)) {
|
||||
const perYear = Math.max(1, rruleListCount(rrule, "BYMONTH"));
|
||||
// Within each BYMONTH month (or the year, without one), the day parts
|
||||
// count as they would for a monthly rule.
|
||||
const months = rruleListCount(rrule, "BYMONTH");
|
||||
const perYear =
|
||||
months > 0
|
||||
? months * occurrencesPerPeriod(rrule, 30, 1)
|
||||
: occurrencesPerPeriod(rrule, 365, 1);
|
||||
return (times * perYear) / (365 * interval);
|
||||
}
|
||||
// FREQ=WEEKLY;BYDAY=…;INTERVAL=… is the modern weekly encoding (the
|
||||
|
||||
Reference in new issue
Block a user