ezmedicationinput 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schedule.js DELETED
@@ -1,1636 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.nextDueDoses = nextDueDoses;
4
- exports.calculateTotalUnits = calculateTotalUnits;
5
- const types_1 = require("./types");
6
- const advice_1 = require("./advice");
7
- const array_1 = require("./utils/array");
8
- const units_1 = require("./utils/units");
9
- const strength_1 = require("./utils/strength");
10
- /**
11
- * Default institution times used when a dosage only specifies frequency without
12
- * explicit EventTiming anchors. Clinics can override these through the
13
- * configuration bag when desired.
14
- */
15
- const DEFAULT_FREQUENCY_DEFAULTS = {
16
- byCode: {
17
- BID: ["08:00", "20:00"],
18
- TID: ["08:00", "14:00", "20:00"],
19
- QID: ["08:00", "12:00", "16:00", "20:00"],
20
- QD: ["09:00"],
21
- QOD: ["09:00"],
22
- AM: ["08:00"],
23
- PM: ["20:00"]
24
- },
25
- byFrequency: {
26
- "freq:1/d": ["09:00"],
27
- "freq:2/d": ["08:00", "20:00"],
28
- "freq:3/d": ["08:00", "14:00", "20:00"],
29
- "freq:4/d": ["08:00", "12:00", "16:00", "20:00"]
30
- }
31
- };
32
- const SECONDS_PER_MINUTE = 60;
33
- const MINUTES_PER_DAY = 24 * 60;
34
- /** Caches expensive Intl.DateTimeFormat objects per time zone. */
35
- const dateTimeFormatCache = new Map();
36
- /** Separate cache for weekday formatting to avoid rebuilding formatters. */
37
- const weekdayFormatCache = new Map();
38
- /** Simple zero-padding helper for numeric components. */
39
- function pad(value, length = 2) {
40
- const absolute = Math.abs(value);
41
- let output = absolute.toString();
42
- while (output.length < length) {
43
- output = `0${output}`;
44
- }
45
- return value < 0 ? `-${output}` : output;
46
- }
47
- function formatToParts(formatter, date) {
48
- const withParts = formatter;
49
- if (typeof withParts.formatToParts === "function") {
50
- return withParts.formatToParts(date);
51
- }
52
- const iso = date.toISOString();
53
- return [
54
- { type: "year", value: iso.slice(0, 4) },
55
- { type: "month", value: iso.slice(5, 7) },
56
- { type: "day", value: iso.slice(8, 10) },
57
- { type: "hour", value: iso.slice(11, 13) },
58
- { type: "minute", value: iso.slice(14, 16) },
59
- { type: "second", value: iso.slice(17, 19) }
60
- ];
61
- }
62
- /**
63
- * Normalizes HH:mm or HH:mm:ss clocks into a consistent HH:mm:ss string.
64
- */
65
- function normalizeClock(clock) {
66
- var _a;
67
- const parts = clock.split(":");
68
- if (parts.length < 2 || parts.length > 3) {
69
- throw new Error(`Invalid clock value: ${clock}`);
70
- }
71
- const [hourPart, minutePart, secondPart] = [
72
- parts[0],
73
- parts[1],
74
- (_a = parts[2]) !== null && _a !== void 0 ? _a : "00"
75
- ];
76
- const hour = Number(hourPart);
77
- const minute = Number(minutePart);
78
- const second = Number(secondPart);
79
- if (Number.isNaN(hour) ||
80
- Number.isNaN(minute) ||
81
- Number.isNaN(second) ||
82
- hour < 0 ||
83
- hour > 23 ||
84
- minute < 0 ||
85
- minute > 59 ||
86
- second < 0 ||
87
- second > 59) {
88
- throw new Error(`Invalid clock value: ${clock}`);
89
- }
90
- return `${pad(hour)}:${pad(minute)}:${pad(second)}`;
91
- }
92
- /** Retrieves (and caches) an Intl formatter for calendar components. */
93
- function getDateTimeFormat(timeZone) {
94
- let formatter = dateTimeFormatCache.get(timeZone);
95
- if (!formatter) {
96
- const options = {
97
- timeZone,
98
- calendar: "iso8601",
99
- numberingSystem: "latn",
100
- hour12: false,
101
- year: "numeric",
102
- month: "2-digit",
103
- day: "2-digit",
104
- hour: "2-digit",
105
- minute: "2-digit",
106
- second: "2-digit"
107
- };
108
- formatter = new Intl.DateTimeFormat("en-CA", options);
109
- dateTimeFormatCache.set(timeZone, formatter);
110
- }
111
- return formatter;
112
- }
113
- /** Retrieves (and caches) a formatter for weekday lookups. */
114
- function getWeekdayFormat(timeZone) {
115
- let formatter = weekdayFormatCache.get(timeZone);
116
- if (!formatter) {
117
- formatter = new Intl.DateTimeFormat("en-CA", {
118
- timeZone,
119
- weekday: "short"
120
- });
121
- weekdayFormatCache.set(timeZone, formatter);
122
- }
123
- return formatter;
124
- }
125
- /**
126
- * Extracts calendar components for a Date interpreted within the supplied time
127
- * zone.
128
- */
129
- function getTimeParts(date, timeZone) {
130
- var _a, _b;
131
- const formatter = getDateTimeFormat(timeZone);
132
- const parts = {};
133
- const rawParts = formatToParts(formatter, date);
134
- for (const part of rawParts) {
135
- if (part.type === "literal") {
136
- continue;
137
- }
138
- if (part.type === "year") {
139
- parts.year = Number(part.value);
140
- }
141
- else if (part.type === "month") {
142
- parts.month = Number(part.value);
143
- }
144
- else if (part.type === "day") {
145
- parts.day = Number(part.value);
146
- }
147
- else if (part.type === "hour") {
148
- parts.hour = Number(part.value);
149
- }
150
- else if (part.type === "minute") {
151
- parts.minute = Number(part.value);
152
- }
153
- else if (part.type === "second") {
154
- parts.second = Number(part.value);
155
- }
156
- }
157
- if (parts.hour === 24) {
158
- // Some locales express midnight as 24:00 of the previous day. Nudge the
159
- // instant forward slightly so we can capture the correct calendar date and
160
- // reset the hour component back to zero.
161
- const forward = new Date(date.getTime() + 60 * 1000);
162
- const forwardParts = formatToParts(formatter, forward);
163
- for (const part of forwardParts) {
164
- if (part.type === "literal") {
165
- continue;
166
- }
167
- if (part.type === "year") {
168
- parts.year = Number(part.value);
169
- }
170
- else if (part.type === "month") {
171
- parts.month = Number(part.value);
172
- }
173
- else if (part.type === "day") {
174
- parts.day = Number(part.value);
175
- }
176
- }
177
- parts.hour = 0;
178
- parts.minute = (_a = parts.minute) !== null && _a !== void 0 ? _a : 0;
179
- parts.second = (_b = parts.second) !== null && _b !== void 0 ? _b : 0;
180
- }
181
- if (parts.year === undefined ||
182
- parts.month === undefined ||
183
- parts.day === undefined ||
184
- parts.hour === undefined ||
185
- parts.minute === undefined ||
186
- parts.second === undefined) {
187
- throw new Error("Unable to resolve time parts for provided date");
188
- }
189
- return parts;
190
- }
191
- /** Calculates the time-zone offset in minutes for a given instant. */
192
- function getOffset(date, timeZone) {
193
- const { year, month, day, hour, minute, second } = getTimeParts(date, timeZone);
194
- const zonedTime = Date.UTC(year, month - 1, day, hour, minute, second);
195
- return (zonedTime - date.getTime()) / (SECONDS_PER_MINUTE * 1000);
196
- }
197
- /**
198
- * Renders an ISO-8601 string that reflects the provided time zone instead of
199
- * defaulting to UTC.
200
- */
201
- function formatZonedIso(date, timeZone) {
202
- const { year, month, day, hour, minute, second } = getTimeParts(date, timeZone);
203
- const offsetMinutes = getOffset(date, timeZone);
204
- const offsetSign = offsetMinutes >= 0 ? "+" : "-";
205
- const absoluteOffset = Math.abs(offsetMinutes);
206
- const offsetHours = Math.floor(absoluteOffset / SECONDS_PER_MINUTE);
207
- const offsetRemainder = absoluteOffset % SECONDS_PER_MINUTE;
208
- return `${pad(year, 4)}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:${pad(second)}${offsetSign}${pad(offsetHours)}:${pad(offsetRemainder)}`;
209
- }
210
- /**
211
- * Builds a Date representing a local wall-clock time in the target time zone.
212
- */
213
- function makeZonedDate(timeZone, year, month, day, hour, minute, second) {
214
- const initialUtc = Date.UTC(year, month - 1, day, hour, minute, second);
215
- let candidate = new Date(initialUtc);
216
- let offset = getOffset(candidate, timeZone);
217
- candidate = new Date(initialUtc - offset * SECONDS_PER_MINUTE * 1000);
218
- const recalculatedOffset = getOffset(candidate, timeZone);
219
- if (recalculatedOffset !== offset) {
220
- candidate = new Date(initialUtc - recalculatedOffset * SECONDS_PER_MINUTE * 1000);
221
- }
222
- const parts = getTimeParts(candidate, timeZone);
223
- if (parts.year !== year ||
224
- parts.month !== month ||
225
- parts.day !== day ||
226
- parts.hour !== hour ||
227
- parts.minute !== minute ||
228
- parts.second !== second) {
229
- return null;
230
- }
231
- return candidate;
232
- }
233
- /** Convenience wrapper around makeZonedDate for day-level math. */
234
- function makeZonedDateFromDay(base, timeZone, clock) {
235
- var _a, _b, _c;
236
- const { year, month, day } = getTimeParts(base, timeZone);
237
- const parts = clock.split(":").map((value) => Number(value));
238
- const hour = (_a = parts[0]) !== null && _a !== void 0 ? _a : 0;
239
- const minute = (_b = parts[1]) !== null && _b !== void 0 ? _b : 0;
240
- const second = (_c = parts[2]) !== null && _c !== void 0 ? _c : 0;
241
- return makeZonedDate(timeZone, year, month, day, hour, minute, second);
242
- }
243
- /** Returns a Date pinned to the start of the local day. */
244
- function startOfLocalDay(date, timeZone) {
245
- const { year, month, day } = getTimeParts(date, timeZone);
246
- const zoned = makeZonedDate(timeZone, year, month, day, 0, 0, 0);
247
- if (!zoned) {
248
- throw new Error("Unable to resolve start of day for provided date");
249
- }
250
- return zoned;
251
- }
252
- /** Adds a number of calendar days while remaining aligned to the time zone. */
253
- function addLocalDays(date, days, timeZone) {
254
- const { year, month, day } = getTimeParts(date, timeZone);
255
- const rollover = new Date(Date.UTC(year, month - 1, day + days));
256
- const zoned = makeZonedDate(timeZone, rollover.getUTCFullYear(), rollover.getUTCMonth() + 1, rollover.getUTCDate(), 0, 0, 0);
257
- if (!zoned) {
258
- throw new Error("Unable to shift local day – invalid calendar combination");
259
- }
260
- return zoned;
261
- }
262
- /** Computes the local weekday token (mon..sun). */
263
- function getLocalWeekday(date, timeZone) {
264
- const formatted = getWeekdayFormat(timeZone).format(date);
265
- switch (formatted.toLowerCase()) {
266
- case "mon":
267
- return "mon";
268
- case "tue":
269
- return "tue";
270
- case "wed":
271
- return "wed";
272
- case "thu":
273
- return "thu";
274
- case "fri":
275
- return "fri";
276
- case "sat":
277
- return "sat";
278
- case "sun":
279
- return "sun";
280
- default:
281
- throw new Error(`Unexpected weekday token: ${formatted}`);
282
- }
283
- }
284
- function getLocalDayNumber(date, timeZone) {
285
- const { year, month, day } = getTimeParts(date, timeZone);
286
- return Math.floor(Date.UTC(year, month - 1, day) / (24 * 60 * 60 * 1000));
287
- }
288
- function getLocalMonthIndex(date, timeZone) {
289
- const { year, month } = getTimeParts(date, timeZone);
290
- return year * 12 + (month - 1);
291
- }
292
- function isDateAlignedToPeriodCycle(candidateDay, anchorDay, repeat, timeZone) {
293
- const period = repeat.period;
294
- const periodUnit = repeat.periodUnit;
295
- if (!period || period <= 0 || !periodUnit) {
296
- return true;
297
- }
298
- if (periodUnit === "d") {
299
- const deltaDays = getLocalDayNumber(candidateDay, timeZone) - getLocalDayNumber(anchorDay, timeZone);
300
- return deltaDays >= 0 && deltaDays % period === 0;
301
- }
302
- if (periodUnit === "wk") {
303
- const deltaDays = getLocalDayNumber(candidateDay, timeZone) - getLocalDayNumber(anchorDay, timeZone);
304
- if (deltaDays < 0) {
305
- return false;
306
- }
307
- const deltaWeeks = Math.floor(deltaDays / 7);
308
- return deltaWeeks % period === 0;
309
- }
310
- if (periodUnit === "mo") {
311
- const deltaMonths = getLocalMonthIndex(candidateDay, timeZone) - getLocalMonthIndex(anchorDay, timeZone);
312
- return deltaMonths >= 0 && deltaMonths % period === 0;
313
- }
314
- if (periodUnit === "a") {
315
- const candidateYear = getTimeParts(candidateDay, timeZone).year;
316
- const anchorYear = getTimeParts(anchorDay, timeZone).year;
317
- const deltaYears = candidateYear - anchorYear;
318
- return deltaYears >= 0 && deltaYears % period === 0;
319
- }
320
- return true;
321
- }
322
- /** Parses arbitrary string/Date inputs into a valid Date instance. */
323
- function coerceDate(value, label) {
324
- const date = value instanceof Date ? value : new Date(value);
325
- if (Number.isNaN(date.getTime())) {
326
- throw new Error(`Invalid ${label} supplied to nextDueDoses`);
327
- }
328
- return date;
329
- }
330
- /**
331
- * Applies a minute offset to a normalized HH:mm:ss clock, tracking any day
332
- * rollover that might occur.
333
- */
334
- function applyOffset(clock, offsetMinutes) {
335
- const [hour, minute, second] = clock.split(":").map((part) => Number(part));
336
- let totalMinutes = hour * SECONDS_PER_MINUTE + minute + offsetMinutes;
337
- let dayShift = 0;
338
- while (totalMinutes < 0) {
339
- totalMinutes += MINUTES_PER_DAY;
340
- dayShift -= 1;
341
- }
342
- while (totalMinutes >= MINUTES_PER_DAY) {
343
- totalMinutes -= MINUTES_PER_DAY;
344
- dayShift += 1;
345
- }
346
- const adjustedHour = Math.floor(totalMinutes / SECONDS_PER_MINUTE);
347
- const adjustedMinute = totalMinutes % SECONDS_PER_MINUTE;
348
- return {
349
- time: `${pad(adjustedHour)}:${pad(adjustedMinute)}:${pad(second)}`,
350
- dayShift
351
- };
352
- }
353
- function parseBoundsDurationUnit(quantity) {
354
- var _a, _b, _c;
355
- const candidate = (_b = (_a = quantity === null || quantity === void 0 ? void 0 : quantity.code) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) !== null && _b !== void 0 ? _b : (_c = quantity === null || quantity === void 0 ? void 0 : quantity.unit) === null || _c === void 0 ? void 0 : _c.trim().toLowerCase();
356
- switch (candidate) {
357
- case "s":
358
- case "sec":
359
- case "second":
360
- case "seconds":
361
- return types_1.FhirPeriodUnit.Second;
362
- case "min":
363
- case "mins":
364
- case "minute":
365
- case "minutes":
366
- return types_1.FhirPeriodUnit.Minute;
367
- case "h":
368
- case "hr":
369
- case "hrs":
370
- case "hour":
371
- case "hours":
372
- return types_1.FhirPeriodUnit.Hour;
373
- case "d":
374
- case "day":
375
- case "days":
376
- return types_1.FhirPeriodUnit.Day;
377
- case "wk":
378
- case "wks":
379
- case "week":
380
- case "weeks":
381
- return types_1.FhirPeriodUnit.Week;
382
- case "mo":
383
- case "month":
384
- case "months":
385
- return types_1.FhirPeriodUnit.Month;
386
- case "a":
387
- case "yr":
388
- case "yrs":
389
- case "year":
390
- case "years":
391
- return types_1.FhirPeriodUnit.Year;
392
- default:
393
- return undefined;
394
- }
395
- }
396
- function resolveRepeatBoundsDuration(repeat) {
397
- var _a, _b, _c, _d;
398
- if (!repeat) {
399
- return {};
400
- }
401
- if (((_a = repeat.boundsDuration) === null || _a === void 0 ? void 0 : _a.value) !== undefined) {
402
- return {
403
- value: repeat.boundsDuration.value,
404
- unit: parseBoundsDurationUnit(repeat.boundsDuration)
405
- };
406
- }
407
- if (!repeat.boundsRange) {
408
- return {};
409
- }
410
- return {
411
- value: (_b = repeat.boundsRange.low) === null || _b === void 0 ? void 0 : _b.value,
412
- max: (_c = repeat.boundsRange.high) === null || _c === void 0 ? void 0 : _c.value,
413
- unit: (_d = parseBoundsDurationUnit(repeat.boundsRange.low)) !== null && _d !== void 0 ? _d : parseBoundsDurationUnit(repeat.boundsRange.high)
414
- };
415
- }
416
- function resolveRepeatDurationCapEnd(repeat, anchor, timeZone) {
417
- var _a, _b;
418
- const bounds = resolveRepeatBoundsDuration(repeat);
419
- const durationValue = (_a = bounds.max) !== null && _a !== void 0 ? _a : bounds.value;
420
- const durationUnit = bounds.unit;
421
- if (durationValue === undefined ||
422
- !Number.isFinite(durationValue) ||
423
- durationValue <= 0 ||
424
- !durationUnit) {
425
- return null;
426
- }
427
- const stepper = createIntervalStepper({ period: durationValue, periodUnit: durationUnit }, timeZone);
428
- if (!stepper) {
429
- return null;
430
- }
431
- return (_b = stepper(anchor)) !== null && _b !== void 0 ? _b : null;
432
- }
433
- function resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter) {
434
- var _a, _b, _c, _d;
435
- if (!enforceDayFilter) {
436
- return undefined;
437
- }
438
- if (repeat.frequency) {
439
- return undefined;
440
- }
441
- if (((_b = (_a = repeat.when) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 || ((_d = (_c = repeat.timeOfDay) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0) {
442
- return undefined;
443
- }
444
- switch (repeat.periodUnit) {
445
- case types_1.FhirPeriodUnit.Week:
446
- case types_1.FhirPeriodUnit.Month:
447
- case types_1.FhirPeriodUnit.Year:
448
- return repeat.period ? repeat : undefined;
449
- case undefined:
450
- return repeat.period === undefined
451
- ? Object.assign(Object.assign({}, repeat), { period: 1, periodUnit: types_1.FhirPeriodUnit.Week }) : undefined;
452
- default:
453
- return undefined;
454
- }
455
- }
456
- function isSingleAdministrationRepeat(repeat) {
457
- var _a, _b, _c, _d, _e, _f;
458
- return (repeat.count === 1 &&
459
- repeat.frequency === undefined &&
460
- repeat.frequencyMax === undefined &&
461
- repeat.period === undefined &&
462
- repeat.periodMax === undefined &&
463
- repeat.periodUnit === undefined &&
464
- ((_b = (_a = repeat.dayOfWeek) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) === 0 &&
465
- ((_d = (_c = repeat.when) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) === 0 &&
466
- ((_f = (_e = repeat.timeOfDay) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0) === 0);
467
- }
468
- function hasUnresolvedRelationalInstruction(dosage) {
469
- var _a, _b, _c, _d, _e, _f;
470
- const texts = [];
471
- if ((_a = dosage.patientInstruction) === null || _a === void 0 ? void 0 : _a.trim()) {
472
- texts.push(dosage.patientInstruction.trim());
473
- }
474
- for (const instruction of (_b = dosage.additionalInstruction) !== null && _b !== void 0 ? _b : []) {
475
- const text = ((_c = instruction.text) === null || _c === void 0 ? void 0 : _c.trim()) || ((_f = (_e = (_d = instruction.coding) === null || _d === void 0 ? void 0 : _d.find((coding) => { var _a; return (_a = coding.display) === null || _a === void 0 ? void 0 : _a.trim(); })) === null || _e === void 0 ? void 0 : _e.display) === null || _f === void 0 ? void 0 : _f.trim());
476
- if (text) {
477
- texts.push(text);
478
- }
479
- }
480
- for (const text of texts) {
481
- const parsed = (0, advice_1.parseAdditionalInstructions)(text, { start: 0, end: text.length }, { defaultPredicate: "take" });
482
- for (const instruction of parsed) {
483
- for (const frame of instruction.frames) {
484
- if (frame.relation) {
485
- return true;
486
- }
487
- }
488
- }
489
- }
490
- return false;
491
- }
492
- function minDate(left, right) {
493
- if (!right) {
494
- return left;
495
- }
496
- return right.getTime() < left.getTime() ? right : left;
497
- }
498
- /** Provides the default meal pairing used for AC/PC expansions. */
499
- function getDefaultMealPairs(config) {
500
- return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner];
501
- }
502
- const SPECIFIC_BEFORE_MEALS = {
503
- [types_1.EventTiming["Before Breakfast"]]: types_1.EventTiming.Breakfast,
504
- [types_1.EventTiming["Before Lunch"]]: types_1.EventTiming.Lunch,
505
- [types_1.EventTiming["Before Dinner"]]: types_1.EventTiming.Dinner
506
- };
507
- const SPECIFIC_AFTER_MEALS = {
508
- [types_1.EventTiming["After Breakfast"]]: types_1.EventTiming.Breakfast,
509
- [types_1.EventTiming["After Lunch"]]: types_1.EventTiming.Lunch,
510
- [types_1.EventTiming["After Dinner"]]: types_1.EventTiming.Dinner
511
- };
512
- /**
513
- * Expands a single EventTiming code into concrete wall-clock entries.
514
- */
515
- function expandTiming(code, config, repeat) {
516
- var _a, _b, _c, _d, _e, _f, _g, _h;
517
- const mealOffsets = (_a = config.mealOffsets) !== null && _a !== void 0 ? _a : {};
518
- const eventClock = (_b = config.eventClock) !== null && _b !== void 0 ? _b : {};
519
- const normalized = [];
520
- const clockValue = eventClock[code];
521
- if (clockValue) {
522
- normalized.push({ time: normalizeClock(clockValue), dayShift: 0 });
523
- }
524
- else if (code === types_1.EventTiming["Before Meal"]) {
525
- for (const meal of getDefaultMealPairs(config)) {
526
- const base = eventClock[meal];
527
- if (!base) {
528
- continue;
529
- }
530
- normalized.push(applyOffset(normalizeClock(base), (_c = mealOffsets[code]) !== null && _c !== void 0 ? _c : 0));
531
- }
532
- }
533
- else if (code === types_1.EventTiming["After Meal"]) {
534
- for (const meal of getDefaultMealPairs(config)) {
535
- const base = eventClock[meal];
536
- if (!base) {
537
- continue;
538
- }
539
- normalized.push(applyOffset(normalizeClock(base), (_d = mealOffsets[code]) !== null && _d !== void 0 ? _d : 0));
540
- }
541
- }
542
- else if (code === types_1.EventTiming.Meal) {
543
- for (const meal of getDefaultMealPairs(config)) {
544
- const base = eventClock[meal];
545
- if (!base) {
546
- continue;
547
- }
548
- normalized.push({ time: normalizeClock(base), dayShift: 0 });
549
- }
550
- }
551
- else if (code in SPECIFIC_BEFORE_MEALS) {
552
- const mealCode = SPECIFIC_BEFORE_MEALS[code];
553
- const base = eventClock[mealCode];
554
- if (base) {
555
- const baseClock = normalizeClock(base);
556
- const offset = (_f = (_e = mealOffsets[code]) !== null && _e !== void 0 ? _e : mealOffsets[types_1.EventTiming["Before Meal"]]) !== null && _f !== void 0 ? _f : 0;
557
- normalized.push(offset ? applyOffset(baseClock, offset) : { time: baseClock, dayShift: 0 });
558
- }
559
- }
560
- else if (code in SPECIFIC_AFTER_MEALS) {
561
- const mealCode = SPECIFIC_AFTER_MEALS[code];
562
- const base = eventClock[mealCode];
563
- if (base) {
564
- const baseClock = normalizeClock(base);
565
- const offset = (_h = (_g = mealOffsets[code]) !== null && _g !== void 0 ? _g : mealOffsets[types_1.EventTiming["After Meal"]]) !== null && _h !== void 0 ? _h : 0;
566
- normalized.push(offset ? applyOffset(baseClock, offset) : { time: baseClock, dayShift: 0 });
567
- }
568
- }
569
- if (repeat.offset && normalized.length) {
570
- return normalized.map((entry) => {
571
- var _a;
572
- const adjusted = applyOffset(entry.time, (_a = repeat.offset) !== null && _a !== void 0 ? _a : 0);
573
- return {
574
- time: adjusted.time,
575
- dayShift: entry.dayShift + adjusted.dayShift
576
- };
577
- });
578
- }
579
- return normalized;
580
- }
581
- /** Consolidates EventTiming arrays into a deduplicated/sorted clock list. */
582
- function expandWhenCodes(whenCodes, config, repeat) {
583
- const entries = [];
584
- const seen = new Set();
585
- for (const code of whenCodes) {
586
- if (code === types_1.EventTiming.Immediate) {
587
- continue;
588
- }
589
- const expansions = expandTiming(code, config, repeat);
590
- for (const expansion of expansions) {
591
- const key = `${expansion.dayShift}|${expansion.time}`;
592
- if (seen.has(key)) {
593
- continue;
594
- }
595
- seen.add(key);
596
- entries.push(expansion);
597
- }
598
- }
599
- return entries.sort((a, b) => {
600
- if (a.dayShift !== b.dayShift) {
601
- return a.dayShift - b.dayShift;
602
- }
603
- return a.time.localeCompare(b.time);
604
- });
605
- }
606
- const DEFAULT_WHEN_FALLBACK_CLOCKS = {
607
- [types_1.EventTiming.Wake]: ["06:00:00"],
608
- [types_1.EventTiming["Early Morning"]]: ["06:00:00"],
609
- [types_1.EventTiming.Morning]: ["08:00:00"],
610
- [types_1.EventTiming["Late Morning"]]: ["10:00:00"],
611
- [types_1.EventTiming.Breakfast]: ["08:00:00"],
612
- [types_1.EventTiming["Before Breakfast"]]: ["07:30:00"],
613
- [types_1.EventTiming["After Breakfast"]]: ["08:30:00"],
614
- [types_1.EventTiming.Noon]: ["12:00:00"],
615
- [types_1.EventTiming.Lunch]: ["12:30:00"],
616
- [types_1.EventTiming["Before Lunch"]]: ["12:00:00"],
617
- [types_1.EventTiming["After Lunch"]]: ["13:00:00"],
618
- [types_1.EventTiming["Early Afternoon"]]: ["14:00:00"],
619
- [types_1.EventTiming.Afternoon]: ["15:00:00"],
620
- [types_1.EventTiming["Late Afternoon"]]: ["16:00:00"],
621
- [types_1.EventTiming["Early Evening"]]: ["18:00:00"],
622
- [types_1.EventTiming.Evening]: ["19:00:00"],
623
- [types_1.EventTiming["Late Evening"]]: ["20:00:00"],
624
- [types_1.EventTiming.Dinner]: ["18:30:00"],
625
- [types_1.EventTiming["Before Dinner"]]: ["18:00:00"],
626
- [types_1.EventTiming["After Dinner"]]: ["19:00:00"],
627
- [types_1.EventTiming.Night]: ["21:00:00"],
628
- [types_1.EventTiming["Before Sleep"]]: ["22:00:00"],
629
- [types_1.EventTiming["After Sleep"]]: ["06:30:00"],
630
- [types_1.EventTiming.Meal]: ["08:00:00", "12:30:00", "18:30:00"],
631
- [types_1.EventTiming["Before Meal"]]: ["07:30:00", "12:00:00", "18:00:00"],
632
- [types_1.EventTiming["After Meal"]]: ["08:30:00", "13:00:00", "19:00:00"]
633
- };
634
- function inferWhenFallbackEntries(whenCodes, repeat) {
635
- const entries = [];
636
- const seen = new Set();
637
- const addClock = (clock) => {
638
- var _a;
639
- const normalized = normalizeClock(clock);
640
- const adjusted = repeat.offset
641
- ? applyOffset(normalized, (_a = repeat.offset) !== null && _a !== void 0 ? _a : 0)
642
- : { time: normalized, dayShift: 0 };
643
- const key = `${adjusted.dayShift}|${adjusted.time}`;
644
- if (seen.has(key)) {
645
- return;
646
- }
647
- seen.add(key);
648
- entries.push(adjusted);
649
- };
650
- for (const code of whenCodes) {
651
- if (code === types_1.EventTiming.Immediate) {
652
- continue;
653
- }
654
- const fallbackClocks = DEFAULT_WHEN_FALLBACK_CLOCKS[code];
655
- if (!fallbackClocks) {
656
- continue;
657
- }
658
- for (const clock of fallbackClocks) {
659
- addClock(clock);
660
- }
661
- }
662
- return entries.sort((a, b) => {
663
- if (a.dayShift !== b.dayShift) {
664
- return a.dayShift - b.dayShift;
665
- }
666
- return a.time.localeCompare(b.time);
667
- });
668
- }
669
- function mergeFrequencyDefaults(base, override) {
670
- var _a, _b, _c, _d;
671
- if (!base && !override) {
672
- return undefined;
673
- }
674
- const merged = {};
675
- if ((base === null || base === void 0 ? void 0 : base.byCode) || (override === null || override === void 0 ? void 0 : override.byCode)) {
676
- merged.byCode = Object.assign(Object.assign({}, ((_a = base === null || base === void 0 ? void 0 : base.byCode) !== null && _a !== void 0 ? _a : {})), ((_b = override === null || override === void 0 ? void 0 : override.byCode) !== null && _b !== void 0 ? _b : {}));
677
- }
678
- if ((base === null || base === void 0 ? void 0 : base.byFrequency) || (override === null || override === void 0 ? void 0 : override.byFrequency)) {
679
- merged.byFrequency = Object.assign(Object.assign({}, ((_c = base === null || base === void 0 ? void 0 : base.byFrequency) !== null && _c !== void 0 ? _c : {})), ((_d = override === null || override === void 0 ? void 0 : override.byFrequency) !== null && _d !== void 0 ? _d : {}));
680
- }
681
- return merged;
682
- }
683
- function inferDailyFrequencyClocks(frequency) {
684
- if (!Number.isFinite(frequency) || frequency <= 0) {
685
- return [];
686
- }
687
- if (frequency === 1) {
688
- return ["09:00:00"];
689
- }
690
- const startMinutes = 8 * 60;
691
- const endMinutes = 20 * 60;
692
- const spanMinutes = endMinutes - startMinutes;
693
- const clocks = new Set();
694
- for (let index = 0; index < frequency; index += 1) {
695
- const minutes = startMinutes + Math.round((spanMinutes * index) / (frequency - 1));
696
- const hour = Math.floor(minutes / SECONDS_PER_MINUTE);
697
- const minute = minutes % SECONDS_PER_MINUTE;
698
- clocks.add(`${pad(hour)}:${pad(minute)}:00`);
699
- }
700
- return Array.from(clocks).sort();
701
- }
702
- /** Resolves fallback clock arrays for frequency-only schedules. */
703
- function resolveFrequencyClocks(timing, config) {
704
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
705
- const defaults = {
706
- byCode: Object.assign(Object.assign({}, DEFAULT_FREQUENCY_DEFAULTS.byCode), ((_b = (_a = config.frequencyDefaults) === null || _a === void 0 ? void 0 : _a.byCode) !== null && _b !== void 0 ? _b : {})),
707
- byFrequency: Object.assign(Object.assign({}, DEFAULT_FREQUENCY_DEFAULTS.byFrequency), ((_d = (_c = config.frequencyDefaults) === null || _c === void 0 ? void 0 : _c.byFrequency) !== null && _d !== void 0 ? _d : {}))
708
- };
709
- const collected = new Set();
710
- const code = (_g = (_f = (_e = timing.code) === null || _e === void 0 ? void 0 : _e.coding) === null || _f === void 0 ? void 0 : _f.find((coding) => coding.code)) === null || _g === void 0 ? void 0 : _g.code;
711
- const normalizedCode = code === null || code === void 0 ? void 0 : code.toUpperCase();
712
- if (normalizedCode && ((_h = defaults.byCode) === null || _h === void 0 ? void 0 : _h[normalizedCode])) {
713
- for (const clock of defaults.byCode[normalizedCode]) {
714
- collected.add(normalizeClock(clock));
715
- }
716
- }
717
- const repeat = timing.repeat;
718
- if ((repeat === null || repeat === void 0 ? void 0 : repeat.frequency) && repeat.period && repeat.periodUnit) {
719
- const key = `freq:${repeat.frequency}/${repeat.periodUnit}`;
720
- if ((_j = defaults.byFrequency) === null || _j === void 0 ? void 0 : _j[key]) {
721
- for (const clock of defaults.byFrequency[key]) {
722
- collected.add(normalizeClock(clock));
723
- }
724
- }
725
- const perPeriodKey = `freq:${repeat.frequency}/per:${repeat.period}${repeat.periodUnit}`;
726
- if ((_k = defaults.byFrequency) === null || _k === void 0 ? void 0 : _k[perPeriodKey]) {
727
- for (const clock of defaults.byFrequency[perPeriodKey]) {
728
- collected.add(normalizeClock(clock));
729
- }
730
- }
731
- if (collected.size === 0 && repeat.period === 1 && repeat.periodUnit === "d") {
732
- for (const clock of inferDailyFrequencyClocks(repeat.frequency)) {
733
- collected.add(clock);
734
- }
735
- }
736
- }
737
- return Array.from(collected).sort();
738
- }
739
- /**
740
- * Produces the next dose timestamps in ascending order according to the
741
- * provided configuration and dosage metadata.
742
- */
743
- function nextDueDoses(dosage, options) {
744
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
745
- if (!options || typeof options !== "object") {
746
- throw new Error("Options argument is required for nextDueDoses");
747
- }
748
- if (options.from === undefined) {
749
- throw new Error("The 'from' option is required for nextDueDoses");
750
- }
751
- const limit = (_a = options.limit) !== null && _a !== void 0 ? _a : 10;
752
- if (!Number.isFinite(limit) || limit <= 0) {
753
- return [];
754
- }
755
- const from = coerceDate(options.from, "from");
756
- const orderedAt = options.orderedAt === undefined ? null : coerceDate(options.orderedAt, "orderedAt");
757
- const priorCountInput = options.priorCount;
758
- if (priorCountInput !== undefined) {
759
- if (!Number.isFinite(priorCountInput) || priorCountInput < 0) {
760
- throw new Error("Invalid priorCount supplied to nextDueDoses");
761
- }
762
- }
763
- let priorCount = priorCountInput !== undefined ? Math.floor(priorCountInput) : 0;
764
- const needsDerivedPriorCount = priorCountInput === undefined && !!orderedAt;
765
- const baseTime = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
766
- const providedConfig = options.config;
767
- const timeZone = (_b = options.timeZone) !== null && _b !== void 0 ? _b : providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.timeZone;
768
- if (!timeZone) {
769
- throw new Error("Configuration with a valid timeZone is required");
770
- }
771
- const eventClock = Object.assign(Object.assign({}, ((_c = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.eventClock) !== null && _c !== void 0 ? _c : {})), ((_d = options.eventClock) !== null && _d !== void 0 ? _d : {}));
772
- const mealOffsets = Object.assign(Object.assign({}, ((_e = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.mealOffsets) !== null && _e !== void 0 ? _e : {})), ((_f = options.mealOffsets) !== null && _f !== void 0 ? _f : {}));
773
- const frequencyDefaults = mergeFrequencyDefaults(providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.frequencyDefaults, options.frequencyDefaults);
774
- const config = {
775
- timeZone,
776
- eventClock,
777
- mealOffsets,
778
- frequencyDefaults
779
- };
780
- const timing = dosage.timing;
781
- const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
782
- const courseEnd = timing && repeat ? resolveRepeatDurationCapEnd(repeat, baseTime, timeZone) : null;
783
- if (needsDerivedPriorCount &&
784
- orderedAt &&
785
- timing &&
786
- repeat &&
787
- repeat.count !== undefined) {
788
- priorCount = derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, timeZone);
789
- }
790
- if (!timing || !repeat) {
791
- return [];
792
- }
793
- if (courseEnd && from >= courseEnd) {
794
- return [];
795
- }
796
- const rawCount = repeat.count;
797
- const normalizedCount = rawCount === undefined ? undefined : Math.max(0, Math.floor(rawCount));
798
- if (normalizedCount === 0) {
799
- return [];
800
- }
801
- const remainingCount = normalizedCount === undefined ? undefined : Math.max(0, normalizedCount - priorCount);
802
- if (remainingCount === 0) {
803
- return [];
804
- }
805
- const effectiveLimit = remainingCount !== undefined ? Math.min(limit, remainingCount) : limit;
806
- if (isSingleAdministrationRepeat(repeat)) {
807
- if (hasUnresolvedRelationalInstruction(dosage)) {
808
- return [];
809
- }
810
- const anchor = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
811
- if ((orderedAt && orderedAt < from) || (courseEnd && anchor >= courseEnd)) {
812
- return [];
813
- }
814
- return [formatZonedIso(anchor, timeZone)].slice(0, effectiveLimit);
815
- }
816
- const results = [];
817
- const seen = new Set();
818
- const dayFilter = new Set(((_g = repeat.dayOfWeek) !== null && _g !== void 0 ? _g : []).map((day) => day.toLowerCase()));
819
- const enforceDayFilter = dayFilter.size > 0;
820
- const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
821
- const whenCodes = (_h = repeat.when) !== null && _h !== void 0 ? _h : [];
822
- const timeOfDayEntries = (_j = repeat.timeOfDay) !== null && _j !== void 0 ? _j : [];
823
- if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
824
- const expanded = expandWhenCodes(whenCodes, config, repeat);
825
- if (timeOfDayEntries.length > 0) {
826
- for (const clock of timeOfDayEntries) {
827
- expanded.push({ time: normalizeClock(clock), dayShift: 0 });
828
- }
829
- expanded.sort((a, b) => {
830
- if (a.dayShift !== b.dayShift) {
831
- return a.dayShift - b.dayShift;
832
- }
833
- return a.time.localeCompare(b.time);
834
- });
835
- }
836
- if (expanded.length === 0 &&
837
- timeOfDayEntries.length === 0 &&
838
- (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
839
- expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
840
- }
841
- const includesImmediate = (0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate);
842
- if (includesImmediate) {
843
- const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
844
- if ((!orderedAt || orderedAt >= from) && (!courseEnd || immediateSource < courseEnd)) {
845
- const instantIso = formatZonedIso(immediateSource, timeZone);
846
- if (!seen.has(instantIso)) {
847
- seen.add(instantIso);
848
- results.push(instantIso);
849
- }
850
- }
851
- }
852
- if (results.length >= effectiveLimit) {
853
- return results.slice(0, effectiveLimit);
854
- }
855
- const canFallbackToFrequency = expanded.length === 0 &&
856
- timeOfDayEntries.length === 0 &&
857
- !!repeat.frequency &&
858
- !!repeat.period &&
859
- !!repeat.periodUnit;
860
- if (!canFallbackToFrequency) {
861
- if (expanded.length === 0) {
862
- return results.slice(0, effectiveLimit);
863
- }
864
- let currentDay = startOfLocalDay(from, timeZone);
865
- let iterations = 0;
866
- const maxIterations = effectiveLimit * 31;
867
- while (results.length < effectiveLimit &&
868
- iterations < maxIterations &&
869
- (!courseEnd || currentDay < courseEnd)) {
870
- const weekday = getLocalWeekday(currentDay, timeZone);
871
- if (!enforceDayFilter || dayFilter.has(weekday)) {
872
- for (const entry of expanded) {
873
- const targetDay = entry.dayShift === 0
874
- ? currentDay
875
- : addLocalDays(currentDay, entry.dayShift, timeZone);
876
- const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
877
- if (!zoned) {
878
- continue;
879
- }
880
- if (zoned < from) {
881
- continue;
882
- }
883
- if (orderedAt && zoned < orderedAt) {
884
- continue;
885
- }
886
- if (courseEnd && zoned >= courseEnd) {
887
- continue;
888
- }
889
- const iso = formatZonedIso(zoned, timeZone);
890
- if (!seen.has(iso)) {
891
- seen.add(iso);
892
- results.push(iso);
893
- if (results.length === effectiveLimit) {
894
- break;
895
- }
896
- }
897
- }
898
- }
899
- if (results.length >= effectiveLimit) {
900
- break;
901
- }
902
- currentDay = addLocalDays(currentDay, 1, timeZone);
903
- iterations += 1;
904
- }
905
- return results.slice(0, effectiveLimit);
906
- }
907
- }
908
- const treatAsInterval = !!repeat.period &&
909
- !!repeat.periodUnit &&
910
- (!repeat.frequency ||
911
- repeat.periodUnit !== "d" ||
912
- (repeat.frequency === 1 && repeat.period > 1));
913
- const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
914
- if (treatAsInterval && supportsDayFilteredInterval) {
915
- // True interval schedules advance from the order start in fixed units. The
916
- // timing.code remains advisory so we only rely on the period/unit fields.
917
- const candidates = generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt, courseEnd);
918
- return candidates;
919
- }
920
- if (dayFilteredSeriesRepeat) {
921
- return generateDayFilteredPeriodSeries({
922
- repeat: dayFilteredSeriesRepeat,
923
- timeZone,
924
- dayFilter,
925
- anchorDay: startOfLocalDay(baseTime, timeZone),
926
- startDay: from,
927
- from,
928
- to: courseEnd !== null && courseEnd !== void 0 ? courseEnd : undefined,
929
- orderedAt,
930
- limit: effectiveLimit,
931
- defaultClock: toLocalClock(baseTime, timeZone)
932
- }).slice(0, effectiveLimit);
933
- }
934
- if (repeat.frequency && repeat.period && repeat.periodUnit) {
935
- // Pure frequency schedules (e.g., BID/TID) rely on institution clocks that
936
- // clinicians expect. These can be overridden via configuration when
937
- // facilities use bespoke medication rounds.
938
- const clocks = resolveFrequencyClocks(timing, config);
939
- if (clocks.length === 0) {
940
- return [];
941
- }
942
- let currentDay = startOfLocalDay(from, timeZone);
943
- let iterations = 0;
944
- const maxIterations = effectiveLimit * 31;
945
- while (results.length < effectiveLimit &&
946
- iterations < maxIterations &&
947
- (!courseEnd || currentDay < courseEnd)) {
948
- const weekday = getLocalWeekday(currentDay, timeZone);
949
- if (!enforceDayFilter || dayFilter.has(weekday)) {
950
- for (const clock of clocks) {
951
- const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
952
- if (!zoned) {
953
- continue;
954
- }
955
- if (zoned < from) {
956
- continue;
957
- }
958
- if (orderedAt && zoned < orderedAt) {
959
- continue;
960
- }
961
- if (courseEnd && zoned >= courseEnd) {
962
- continue;
963
- }
964
- const iso = formatZonedIso(zoned, timeZone);
965
- if (!seen.has(iso)) {
966
- seen.add(iso);
967
- results.push(iso);
968
- if (results.length === effectiveLimit) {
969
- break;
970
- }
971
- }
972
- }
973
- }
974
- currentDay = addLocalDays(currentDay, 1, timeZone);
975
- iterations += 1;
976
- }
977
- return results.slice(0, effectiveLimit);
978
- }
979
- return [];
980
- }
981
- function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, timeZone) {
982
- var _a, _b, _c;
983
- if (from <= orderedAt) {
984
- return 0;
985
- }
986
- const normalizedCount = repeat.count === undefined
987
- ? undefined
988
- : Math.max(0, Math.floor(repeat.count));
989
- if (normalizedCount === 0) {
990
- return 0;
991
- }
992
- const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
993
- const enforceDayFilter = dayFilter.size > 0;
994
- const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
995
- const seen = new Set();
996
- let count = 0;
997
- const recordCandidate = (candidate) => {
998
- if (!candidate) {
999
- return false;
1000
- }
1001
- if (candidate < orderedAt || candidate >= from) {
1002
- return false;
1003
- }
1004
- const iso = formatZonedIso(candidate, timeZone);
1005
- if (seen.has(iso)) {
1006
- return false;
1007
- }
1008
- seen.add(iso);
1009
- count += 1;
1010
- return true;
1011
- };
1012
- const whenCodes = (_b = repeat.when) !== null && _b !== void 0 ? _b : [];
1013
- const timeOfDayEntries = (_c = repeat.timeOfDay) !== null && _c !== void 0 ? _c : [];
1014
- if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
1015
- const expanded = expandWhenCodes(whenCodes, config, repeat);
1016
- if (timeOfDayEntries.length > 0) {
1017
- for (const clock of timeOfDayEntries) {
1018
- expanded.push({ time: normalizeClock(clock), dayShift: 0 });
1019
- }
1020
- expanded.sort((a, b) => {
1021
- if (a.dayShift !== b.dayShift) {
1022
- return a.dayShift - b.dayShift;
1023
- }
1024
- return a.time.localeCompare(b.time);
1025
- });
1026
- }
1027
- if (expanded.length === 0 &&
1028
- timeOfDayEntries.length === 0 &&
1029
- (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
1030
- expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
1031
- }
1032
- if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
1033
- if (recordCandidate(orderedAt) && normalizedCount !== undefined && seen.size >= normalizedCount) {
1034
- return count;
1035
- }
1036
- }
1037
- const canFallbackToFrequency = expanded.length === 0 &&
1038
- timeOfDayEntries.length === 0 &&
1039
- !!repeat.frequency &&
1040
- !!repeat.period &&
1041
- !!repeat.periodUnit;
1042
- if (!canFallbackToFrequency) {
1043
- if (expanded.length === 0) {
1044
- return count;
1045
- }
1046
- let currentDay = startOfLocalDay(orderedAt, timeZone);
1047
- let iterations = 0;
1048
- const maxIterations = normalizedCount !== undefined ? normalizedCount * 31 : 31 * 365;
1049
- while (currentDay < from && iterations < maxIterations) {
1050
- const weekday = getLocalWeekday(currentDay, timeZone);
1051
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1052
- for (const entry of expanded) {
1053
- const targetDay = entry.dayShift === 0
1054
- ? currentDay
1055
- : addLocalDays(currentDay, entry.dayShift, timeZone);
1056
- const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
1057
- if (!zoned) {
1058
- continue;
1059
- }
1060
- if (zoned < orderedAt || zoned >= from) {
1061
- continue;
1062
- }
1063
- if (recordCandidate(zoned) && normalizedCount !== undefined && seen.size >= normalizedCount) {
1064
- return count;
1065
- }
1066
- }
1067
- }
1068
- currentDay = addLocalDays(currentDay, 1, timeZone);
1069
- iterations += 1;
1070
- }
1071
- return count;
1072
- }
1073
- }
1074
- const treatAsInterval = !!repeat.period &&
1075
- !!repeat.periodUnit &&
1076
- (!repeat.frequency ||
1077
- repeat.periodUnit !== "d" ||
1078
- (repeat.frequency === 1 && repeat.period > 1));
1079
- const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
1080
- if (treatAsInterval && supportsDayFilteredInterval) {
1081
- const increment = createIntervalStepper(repeat, timeZone);
1082
- if (!increment) {
1083
- return count;
1084
- }
1085
- let current = orderedAt;
1086
- let guard = 0;
1087
- const maxIterations = normalizedCount !== undefined ? normalizedCount * 1000 : 1000;
1088
- while (current < from && guard < maxIterations) {
1089
- const weekday = getLocalWeekday(current, timeZone);
1090
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1091
- if (recordCandidate(current) && normalizedCount !== undefined && seen.size >= normalizedCount) {
1092
- return count;
1093
- }
1094
- }
1095
- const next = increment(current);
1096
- if (!next || next.getTime() === current.getTime()) {
1097
- break;
1098
- }
1099
- current = next;
1100
- guard += 1;
1101
- }
1102
- return count;
1103
- }
1104
- if (dayFilteredSeriesRepeat) {
1105
- const generated = generateDayFilteredPeriodSeries({
1106
- repeat: dayFilteredSeriesRepeat,
1107
- timeZone,
1108
- dayFilter,
1109
- anchorDay: startOfLocalDay(orderedAt, timeZone),
1110
- startDay: orderedAt,
1111
- from: orderedAt,
1112
- to: from,
1113
- orderedAt,
1114
- limit: normalizedCount !== null && normalizedCount !== void 0 ? normalizedCount : 31 * 365,
1115
- defaultClock: toLocalClock(orderedAt, timeZone)
1116
- });
1117
- return count + generated.length;
1118
- }
1119
- if (repeat.frequency && repeat.period && repeat.periodUnit) {
1120
- const clocks = resolveFrequencyClocks(timing, config);
1121
- if (clocks.length === 0) {
1122
- return count;
1123
- }
1124
- let currentDay = startOfLocalDay(orderedAt, timeZone);
1125
- let iterations = 0;
1126
- const maxIterations = normalizedCount !== undefined ? normalizedCount * 31 : 31 * 365;
1127
- while (currentDay < from && iterations < maxIterations) {
1128
- const weekday = getLocalWeekday(currentDay, timeZone);
1129
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1130
- for (const clock of clocks) {
1131
- const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
1132
- if (!zoned) {
1133
- continue;
1134
- }
1135
- if (zoned < orderedAt || zoned >= from) {
1136
- continue;
1137
- }
1138
- if (recordCandidate(zoned) && normalizedCount !== undefined && seen.size >= normalizedCount) {
1139
- return count;
1140
- }
1141
- }
1142
- }
1143
- currentDay = addLocalDays(currentDay, 1, timeZone);
1144
- iterations += 1;
1145
- }
1146
- }
1147
- return count;
1148
- }
1149
- /**
1150
- * Generates an interval-based series by stepping forward from the base time
1151
- * until the requested number of timestamps have been produced.
1152
- */
1153
- function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt, upperBound) {
1154
- const increment = createIntervalStepper(repeat, timeZone);
1155
- if (!increment) {
1156
- return [];
1157
- }
1158
- const results = [];
1159
- const seen = new Set();
1160
- let current = baseTime;
1161
- let guard = 0;
1162
- const maxIterations = effectiveLimit * 1000;
1163
- while (current < from && guard < maxIterations) {
1164
- const next = increment(current);
1165
- if (!next || next.getTime() === current.getTime()) {
1166
- break;
1167
- }
1168
- current = next;
1169
- guard += 1;
1170
- }
1171
- while (results.length < effectiveLimit &&
1172
- guard < maxIterations &&
1173
- (!upperBound || current < upperBound)) {
1174
- const weekday = getLocalWeekday(current, timeZone);
1175
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1176
- if (current < from) {
1177
- // Ensure the current candidate respects the evaluation window.
1178
- guard += 1;
1179
- const next = increment(current);
1180
- if (!next || next.getTime() === current.getTime()) {
1181
- break;
1182
- }
1183
- current = next;
1184
- continue;
1185
- }
1186
- if (orderedAt && current < orderedAt) {
1187
- guard += 1;
1188
- const next = increment(current);
1189
- if (!next || next.getTime() === current.getTime()) {
1190
- break;
1191
- }
1192
- current = next;
1193
- continue;
1194
- }
1195
- if (upperBound && current >= upperBound) {
1196
- break;
1197
- }
1198
- const iso = formatZonedIso(current, timeZone);
1199
- if (!seen.has(iso)) {
1200
- seen.add(iso);
1201
- results.push(iso);
1202
- }
1203
- }
1204
- const next = increment(current);
1205
- if (!next || next.getTime() === current.getTime()) {
1206
- break;
1207
- }
1208
- current = next;
1209
- guard += 1;
1210
- }
1211
- return results.slice(0, effectiveLimit);
1212
- }
1213
- /**
1214
- * Builds a function that advances a Date according to repeat.period/unit.
1215
- *
1216
- * @param repeat FHIR repeat object containing `period` and `periodUnit`.
1217
- * @param timeZone IANA timezone used for calendar-aware month/year stepping.
1218
- * @returns Stepper function advancing one interval, or `null` when unsupported.
1219
- */
1220
- function createIntervalStepper(repeat, timeZone) {
1221
- const { period, periodUnit } = repeat;
1222
- if (!period || !periodUnit) {
1223
- return null;
1224
- }
1225
- if (periodUnit === "s" || periodUnit === "min" || periodUnit === "h") {
1226
- const multiplier = periodUnit === "s" ? 1000 : periodUnit === "min" ? 60 * 1000 : 60 * 60 * 1000;
1227
- const delta = period * multiplier;
1228
- return (value) => new Date(value.getTime() + delta);
1229
- }
1230
- if (periodUnit === "d") {
1231
- const delta = period * 24 * 60 * 60 * 1000;
1232
- return (value) => new Date(value.getTime() + delta);
1233
- }
1234
- if (periodUnit === "wk") {
1235
- const delta = period * 7 * 24 * 60 * 60 * 1000;
1236
- return (value) => new Date(value.getTime() + delta);
1237
- }
1238
- if (periodUnit === "mo") {
1239
- return (value) => addCalendarMonths(value, period, timeZone);
1240
- }
1241
- if (periodUnit === "a") {
1242
- return (value) => addCalendarMonths(value, period * 12, timeZone);
1243
- }
1244
- return null;
1245
- }
1246
- /**
1247
- * Adds calendar months while respecting varying month lengths and DST.
1248
- *
1249
- * @param date Starting instant.
1250
- * @param months Number of calendar months to add.
1251
- * @param timeZone IANA timezone used for wall-clock preservation.
1252
- * @returns Shifted date preserving local clock and clamped day-of-month.
1253
- */
1254
- function addCalendarMonths(date, months, timeZone) {
1255
- const { year, month, day, hour, minute, second } = getTimeParts(date, timeZone);
1256
- const targetMonthIndex = month - 1 + months;
1257
- const targetYear = year + Math.floor(targetMonthIndex / 12);
1258
- const targetMonth = (targetMonthIndex % 12 + 12) % 12;
1259
- const candidate = makeZonedDate(timeZone, targetYear, targetMonth + 1, 1, hour, minute, second);
1260
- if (!candidate) {
1261
- throw new Error("Unable to compute candidate month while scheduling");
1262
- }
1263
- const lastDay = new Date(candidate.getTime());
1264
- lastDay.setUTCMonth(lastDay.getUTCMonth() + 1);
1265
- lastDay.setUTCDate(0);
1266
- const maxDay = getTimeParts(lastDay, timeZone).day;
1267
- const resolvedDay = Math.min(day, maxDay);
1268
- const final = makeZonedDate(timeZone, targetYear, targetMonth + 1, resolvedDay, hour, minute, second);
1269
- if (!final) {
1270
- throw new Error("Unable to resolve monthly advancement – invalid calendar date");
1271
- }
1272
- return final;
1273
- }
1274
- /**
1275
- * Determines whether interval stepping can safely combine with a day-of-week filter.
1276
- *
1277
- * @param repeat FHIR timing repeat object containing period unit metadata.
1278
- * @param enforceDayFilter Whether a `dayOfWeek` filter is active for this schedule.
1279
- * @returns `true` when interval stepping should be used directly; otherwise fallback logic is required.
1280
- */
1281
- function isDayFilteredIntervalSupported(repeat, enforceDayFilter) {
1282
- if (!enforceDayFilter) {
1283
- return true;
1284
- }
1285
- return (repeat.periodUnit === "s" ||
1286
- repeat.periodUnit === "min" ||
1287
- repeat.periodUnit === "h" ||
1288
- repeat.periodUnit === "d");
1289
- }
1290
- /**
1291
- * Expands weekly/monthly/yearly day-filtered schedules into concrete dose timestamps.
1292
- *
1293
- * @param options Configuration describing bounds, cadence, and clock defaults.
1294
- * @param options.repeat FHIR repeat block driving cadence and cycle alignment.
1295
- * @param options.timeZone IANA timezone used for local weekday and clock resolution.
1296
- * @param options.dayFilter Set of lowercased weekdays that are allowed (e.g. `mon`, `tue`).
1297
- * @param options.anchorDay Cycle anchor day used to determine period alignment.
1298
- * @param options.startDay First local day to begin scanning.
1299
- * @param options.from Inclusive lower bound for candidate timestamps.
1300
- * @param options.to Optional exclusive upper bound for candidate timestamps.
1301
- * @param options.orderedAt Optional lower bound representing the order start.
1302
- * @param options.limit Maximum number of timestamps to emit.
1303
- * @param options.defaultClock Default local clock (`HH:MM:SS`) when no explicit clock is provided.
1304
- * @returns Sorted zoned ISO timestamps matching the requested cadence and bounds.
1305
- */
1306
- function generateDayFilteredPeriodSeries(options) {
1307
- const { repeat, timeZone, dayFilter, anchorDay, startDay, from, to, orderedAt, limit, defaultClock } = options;
1308
- if (limit <= 0) {
1309
- return [];
1310
- }
1311
- const results = [];
1312
- const seen = new Set();
1313
- const clocks = [defaultClock !== null && defaultClock !== void 0 ? defaultClock : "08:00:00"];
1314
- let currentDay = startOfLocalDay(startDay, timeZone);
1315
- let iterations = 0;
1316
- const startMs = currentDay.getTime();
1317
- const estimatedDays = to
1318
- ? Math.max(1, Math.ceil((to.getTime() - startMs) / (24 * 60 * 60 * 1000)))
1319
- : limit * 31;
1320
- const maxIterations = Math.max(limit * 31, estimatedDays + 31);
1321
- while (results.length < limit &&
1322
- iterations < maxIterations &&
1323
- (!to || currentDay < to)) {
1324
- const weekday = getLocalWeekday(currentDay, timeZone);
1325
- const inPeriodCycle = isDateAlignedToPeriodCycle(currentDay, anchorDay, repeat, timeZone);
1326
- if (inPeriodCycle && dayFilter.has(weekday)) {
1327
- for (const clock of clocks) {
1328
- const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
1329
- if (!zoned) {
1330
- continue;
1331
- }
1332
- if (zoned < from) {
1333
- continue;
1334
- }
1335
- if (to && zoned >= to) {
1336
- continue;
1337
- }
1338
- if (orderedAt && zoned < orderedAt) {
1339
- continue;
1340
- }
1341
- const iso = formatZonedIso(zoned, timeZone);
1342
- if (!seen.has(iso)) {
1343
- seen.add(iso);
1344
- results.push(iso);
1345
- if (results.length === limit) {
1346
- break;
1347
- }
1348
- }
1349
- }
1350
- }
1351
- currentDay = addLocalDays(currentDay, 1, timeZone);
1352
- iterations += 1;
1353
- }
1354
- return results;
1355
- }
1356
- /**
1357
- * Formats a date into a local `HH:MM:SS` clock for the supplied timezone.
1358
- *
1359
- * @param date Source instant.
1360
- * @param timeZone IANA timezone to interpret the instant.
1361
- * @returns Local wall-clock time in `HH:MM:SS` format.
1362
- */
1363
- function toLocalClock(date, timeZone) {
1364
- const parts = getTimeParts(date, timeZone);
1365
- const twoDigits = (value) => (value < 10 ? `0${value}` : `${value}`);
1366
- const h = twoDigits(parts.hour);
1367
- const m = twoDigits(parts.minute);
1368
- const s = twoDigits(parts.second);
1369
- return `${h}:${m}:${s}`;
1370
- }
1371
- /**
1372
- * Internal helper to count dose events within a time range.
1373
- *
1374
- * @param dosage Dosage definition with timing metadata.
1375
- * @param from Inclusive lower time bound for counting.
1376
- * @param to Exclusive upper time bound for counting.
1377
- * @param config Scheduling configuration (timezone, clocks, offsets).
1378
- * @param baseTime Anchor instant used for interval alignment.
1379
- * @param orderedAt Optional order timestamp used as an additional lower bound.
1380
- * @param limit Optional hard cap on emitted candidates to avoid runaway loops.
1381
- * @returns Number of unique dose events in the requested window.
1382
- */
1383
- function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limit) {
1384
- var _a, _b, _c;
1385
- const timing = dosage.timing;
1386
- const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
1387
- if (!timing || !repeat)
1388
- return 0;
1389
- const normalizedCount = repeat.count === undefined
1390
- ? undefined
1391
- : Math.max(0, Math.floor(repeat.count));
1392
- if (normalizedCount === 0) {
1393
- return 0;
1394
- }
1395
- const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
1396
- const enforceDayFilter = dayFilter.size > 0;
1397
- const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
1398
- const seen = new Set();
1399
- let count = 0;
1400
- const timeZone = config.timeZone;
1401
- const priorCount = normalizedCount !== undefined && orderedAt && from > orderedAt
1402
- ? derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, timeZone)
1403
- : 0;
1404
- const countLimit = normalizedCount === undefined
1405
- ? (limit !== null && limit !== void 0 ? limit : Number.POSITIVE_INFINITY)
1406
- : Math.min(limit !== null && limit !== void 0 ? limit : normalizedCount, Math.max(0, normalizedCount - priorCount));
1407
- if (countLimit <= 0) {
1408
- return 0;
1409
- }
1410
- const hardLimit = Number.isFinite(countLimit) ? countLimit : 365 * 31;
1411
- if (isSingleAdministrationRepeat(repeat)) {
1412
- if (hasUnresolvedRelationalInstruction(dosage)) {
1413
- return 0;
1414
- }
1415
- const anchor = orderedAt !== null && orderedAt !== void 0 ? orderedAt : baseTime;
1416
- if (anchor < from || anchor >= to) {
1417
- return 0;
1418
- }
1419
- return 1;
1420
- }
1421
- const recordCandidate = (candidate) => {
1422
- if (!candidate)
1423
- return false;
1424
- if (candidate < from || candidate >= to)
1425
- return false;
1426
- const iso = formatZonedIso(candidate, timeZone);
1427
- if (seen.has(iso))
1428
- return false;
1429
- seen.add(iso);
1430
- count += 1;
1431
- return true;
1432
- };
1433
- const whenCodes = (_b = repeat.when) !== null && _b !== void 0 ? _b : [];
1434
- const timeOfDayEntries = (_c = repeat.timeOfDay) !== null && _c !== void 0 ? _c : [];
1435
- if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
1436
- const expanded = expandWhenCodes(whenCodes, config, repeat);
1437
- if (timeOfDayEntries.length > 0) {
1438
- for (const clock of timeOfDayEntries) {
1439
- expanded.push({ time: normalizeClock(clock), dayShift: 0 });
1440
- }
1441
- expanded.sort((a, b) => {
1442
- if (a.dayShift !== b.dayShift)
1443
- return a.dayShift - b.dayShift;
1444
- return a.time.localeCompare(b.time);
1445
- });
1446
- }
1447
- if (expanded.length === 0 &&
1448
- timeOfDayEntries.length === 0 &&
1449
- (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
1450
- expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
1451
- }
1452
- if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
1453
- const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
1454
- if (!orderedAt || orderedAt >= from) {
1455
- recordCandidate(immediateSource);
1456
- }
1457
- }
1458
- const canFallbackToFrequency = expanded.length === 0 &&
1459
- timeOfDayEntries.length === 0 &&
1460
- !!repeat.frequency &&
1461
- !!repeat.period &&
1462
- !!repeat.periodUnit;
1463
- if (!canFallbackToFrequency) {
1464
- if (expanded.length === 0)
1465
- return count;
1466
- let currentDay = startOfLocalDay(from, timeZone);
1467
- let iterations = 0;
1468
- const maxIterations = hardLimit * 31;
1469
- while (count < countLimit && currentDay < to && iterations < maxIterations) {
1470
- const weekday = getLocalWeekday(currentDay, timeZone);
1471
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1472
- for (const entry of expanded) {
1473
- const targetDay = entry.dayShift === 0
1474
- ? currentDay
1475
- : addLocalDays(currentDay, entry.dayShift, timeZone);
1476
- const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
1477
- if (zoned)
1478
- recordCandidate(zoned);
1479
- }
1480
- }
1481
- currentDay = addLocalDays(currentDay, 1, timeZone);
1482
- iterations += 1;
1483
- }
1484
- return count;
1485
- }
1486
- }
1487
- const treatAsInterval = !!repeat.period &&
1488
- !!repeat.periodUnit &&
1489
- (!repeat.frequency ||
1490
- repeat.periodUnit !== "d" ||
1491
- (repeat.frequency === 1 && repeat.period > 1));
1492
- const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
1493
- if (treatAsInterval && supportsDayFilteredInterval) {
1494
- const increment = createIntervalStepper(repeat, timeZone);
1495
- if (!increment)
1496
- return count;
1497
- let current = baseTime;
1498
- let guard = 0;
1499
- const maxIterations = hardLimit * 1000;
1500
- // Advance to "from"
1501
- while (current < from && guard < maxIterations) {
1502
- const next = increment(current);
1503
- if (!next || next.getTime() === current.getTime())
1504
- break;
1505
- current = next;
1506
- guard++;
1507
- }
1508
- while (current < to && count < countLimit && guard < maxIterations) {
1509
- const weekday = getLocalWeekday(current, timeZone);
1510
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1511
- recordCandidate(current);
1512
- }
1513
- const next = increment(current);
1514
- if (!next || next.getTime() === current.getTime())
1515
- break;
1516
- current = next;
1517
- guard += 1;
1518
- }
1519
- return count;
1520
- }
1521
- if (repeat.frequency && repeat.period && repeat.periodUnit) {
1522
- const clocks = resolveFrequencyClocks(timing, config);
1523
- if (clocks.length === 0)
1524
- return count;
1525
- let currentDay = startOfLocalDay(from, timeZone);
1526
- let iterations = 0;
1527
- const maxIterations = hardLimit * 31;
1528
- while (count < countLimit && currentDay < to && iterations < maxIterations) {
1529
- const weekday = getLocalWeekday(currentDay, timeZone);
1530
- if (!enforceDayFilter || dayFilter.has(weekday)) {
1531
- for (const clock of clocks) {
1532
- const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
1533
- if (zoned)
1534
- recordCandidate(zoned);
1535
- }
1536
- }
1537
- currentDay = addLocalDays(currentDay, 1, timeZone);
1538
- iterations += 1;
1539
- }
1540
- }
1541
- // Fallback for dayOfWeek with period/periodUnit but no explicit frequency/clocks
1542
- if (dayFilteredSeriesRepeat) {
1543
- const generated = generateDayFilteredPeriodSeries({
1544
- repeat: dayFilteredSeriesRepeat,
1545
- timeZone,
1546
- dayFilter,
1547
- anchorDay: startOfLocalDay(baseTime, timeZone),
1548
- startDay: from,
1549
- from,
1550
- to,
1551
- orderedAt,
1552
- limit: hardLimit,
1553
- defaultClock: toLocalClock(baseTime, timeZone)
1554
- });
1555
- return count + generated.length;
1556
- }
1557
- return count;
1558
- }
1559
- function calculateTotalUnitsSingle(options) {
1560
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1561
- const { dosage, durationValue, durationUnit, roundToMultiple, context } = options;
1562
- const from = coerceDate(options.from, "from");
1563
- const orderedAtDate = options.orderedAt === undefined ? null : coerceDate(options.orderedAt, "orderedAt");
1564
- const providedConfig = options.config;
1565
- const timeZone = (_a = options.timeZone) !== null && _a !== void 0 ? _a : providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.timeZone;
1566
- if (!timeZone) {
1567
- throw new Error("timeZone is required for calculateTotalUnits");
1568
- }
1569
- const eventClock = Object.assign(Object.assign({}, ((_b = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.eventClock) !== null && _b !== void 0 ? _b : {})), ((_c = options.eventClock) !== null && _c !== void 0 ? _c : {}));
1570
- const mealOffsets = Object.assign(Object.assign({}, ((_d = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.mealOffsets) !== null && _d !== void 0 ? _d : {})), ((_e = options.mealOffsets) !== null && _e !== void 0 ? _e : {}));
1571
- const frequencyDefaults = mergeFrequencyDefaults(providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.frequencyDefaults, options.frequencyDefaults);
1572
- const config = {
1573
- timeZone,
1574
- eventClock,
1575
- mealOffsets,
1576
- frequencyDefaults
1577
- };
1578
- // Calculate end date based on duration
1579
- let endDay;
1580
- const dummyRepeat = { period: durationValue, periodUnit: durationUnit };
1581
- const stepper = createIntervalStepper(dummyRepeat, timeZone);
1582
- if (stepper) {
1583
- endDay = stepper(from) || from;
1584
- }
1585
- else {
1586
- endDay = from;
1587
- }
1588
- endDay = minDate(endDay, resolveRepeatDurationCapEnd((_f = dosage.timing) === null || _f === void 0 ? void 0 : _f.repeat, orderedAtDate !== null && orderedAtDate !== void 0 ? orderedAtDate : from, timeZone));
1589
- const count = countScheduleEvents(dosage, from, endDay, config, orderedAtDate !== null && orderedAtDate !== void 0 ? orderedAtDate : from, orderedAtDate, 2000);
1590
- const doseQuantity = (_k = (_j = (_h = (_g = dosage.doseAndRate) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.doseQuantity) === null || _j === void 0 ? void 0 : _j.value) !== null && _k !== void 0 ? _k : 0;
1591
- let totalUnits = count * doseQuantity;
1592
- if (roundToMultiple && roundToMultiple > 0) {
1593
- totalUnits = Math.ceil(totalUnits / roundToMultiple) * roundToMultiple;
1594
- }
1595
- const result = { totalUnits };
1596
- // Handle containers
1597
- const containerValue = context === null || context === void 0 ? void 0 : context.containerValue;
1598
- const containerUnit = context === null || context === void 0 ? void 0 : context.containerUnit;
1599
- const doseUnit = (_o = (_m = (_l = dosage.doseAndRate) === null || _l === void 0 ? void 0 : _l[0]) === null || _m === void 0 ? void 0 : _m.doseQuantity) === null || _o === void 0 ? void 0 : _o.unit;
1600
- if (containerValue && containerValue > 0) {
1601
- let effectiveUnits = totalUnits;
1602
- if (containerUnit && doseUnit && containerUnit !== doseUnit) {
1603
- let strength = context === null || context === void 0 ? void 0 : context.strengthRatio;
1604
- if (!strength && (context === null || context === void 0 ? void 0 : context.strength)) {
1605
- strength = (0, strength_1.parseStrengthIntoRatio)(context.strength, context) || undefined;
1606
- }
1607
- const converted = (0, units_1.convertValue)(totalUnits, doseUnit, containerUnit, strength);
1608
- if (converted !== null) {
1609
- effectiveUnits = converted;
1610
- }
1611
- }
1612
- result.totalContainers = Math.ceil(effectiveUnits / containerValue);
1613
- }
1614
- return result;
1615
- }
1616
- function calculateTotalUnits(options) {
1617
- if (Array.isArray(options.dosage)) {
1618
- const hasAnyDosage = options.dosage.length > 0;
1619
- if (!hasAnyDosage) {
1620
- return { totalUnits: 0 };
1621
- }
1622
- let totalUnits = 0;
1623
- let totalContainers = 0;
1624
- let sawContainers = false;
1625
- for (const dosage of options.dosage) {
1626
- const result = calculateTotalUnitsSingle(Object.assign(Object.assign({}, options), { dosage }));
1627
- totalUnits += result.totalUnits;
1628
- if (result.totalContainers !== undefined) {
1629
- totalContainers += result.totalContainers;
1630
- sawContainers = true;
1631
- }
1632
- }
1633
- return sawContainers ? { totalUnits, totalContainers } : { totalUnits };
1634
- }
1635
- return calculateTotalUnitsSingle(options);
1636
- }