ezmedicationinput 0.1.42 → 0.1.44

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.
Files changed (46) hide show
  1. package/README.md +31 -1
  2. package/dist/advice-rules.json +772 -0
  3. package/dist/advice-terminology.json +104 -0
  4. package/dist/advice.d.ts +16 -0
  5. package/dist/advice.js +1375 -0
  6. package/dist/event-trigger.d.ts +14 -0
  7. package/dist/event-trigger.js +501 -0
  8. package/dist/fhir-translations.d.ts +5 -0
  9. package/dist/fhir-translations.js +117 -0
  10. package/dist/fhir.d.ts +6 -4
  11. package/dist/fhir.js +566 -134
  12. package/dist/format.d.ts +5 -2
  13. package/dist/format.js +581 -219
  14. package/dist/i18n.d.ts +4 -2
  15. package/dist/i18n.js +725 -197
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +221 -169
  18. package/dist/internal-types.d.ts +5 -5
  19. package/dist/ir.d.ts +4 -0
  20. package/dist/ir.js +178 -0
  21. package/dist/lexer/lex.d.ts +2 -0
  22. package/dist/lexer/lex.js +401 -0
  23. package/dist/lexer/meaning.d.ts +71 -0
  24. package/dist/lexer/meaning.js +619 -0
  25. package/dist/lexer/surface.d.ts +2 -0
  26. package/dist/lexer/surface.js +62 -0
  27. package/dist/lexer/token-types.d.ts +36 -0
  28. package/dist/lexer/token-types.js +19 -0
  29. package/dist/maps.d.ts +6 -12
  30. package/dist/maps.js +793 -247
  31. package/dist/parser-state.d.ts +101 -0
  32. package/dist/parser-state.js +441 -0
  33. package/dist/parser.d.ts +7 -7
  34. package/dist/parser.js +3598 -1974
  35. package/dist/prn.d.ts +4 -0
  36. package/dist/prn.js +59 -0
  37. package/dist/schedule.js +230 -32
  38. package/dist/site-phrases.d.ts +35 -0
  39. package/dist/site-phrases.js +344 -0
  40. package/dist/timing-summary.d.ts +25 -0
  41. package/dist/timing-summary.js +138 -0
  42. package/dist/types.d.ts +248 -32
  43. package/dist/types.js +49 -1
  44. package/dist/utils/text.d.ts +3 -0
  45. package/dist/utils/text.js +48 -0
  46. package/package.json +1 -1
package/dist/prn.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { CanonicalPrnReasonExpr } from "./types";
2
+ export declare function getCanonicalPrnReasonText(reason: CanonicalPrnReasonExpr | undefined): string | undefined;
3
+ export declare function joinCanonicalPrnReasonTexts(reasons: CanonicalPrnReasonExpr[] | undefined, conjunction?: string): string | undefined;
4
+ export declare function getPreferredCanonicalPrnReasonText(reason: CanonicalPrnReasonExpr | undefined, reasons: CanonicalPrnReasonExpr[] | undefined, conjunction?: string): string | undefined;
package/dist/prn.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCanonicalPrnReasonText = getCanonicalPrnReasonText;
4
+ exports.joinCanonicalPrnReasonTexts = joinCanonicalPrnReasonTexts;
5
+ exports.getPreferredCanonicalPrnReasonText = getPreferredCanonicalPrnReasonText;
6
+ function getCanonicalPrnReasonText(reason) {
7
+ var _a, _b;
8
+ return (_a = reason === null || reason === void 0 ? void 0 : reason.text) !== null && _a !== void 0 ? _a : (_b = reason === null || reason === void 0 ? void 0 : reason.coding) === null || _b === void 0 ? void 0 : _b.display;
9
+ }
10
+ function joinCanonicalPrnReasonTexts(reasons, conjunction = "or") {
11
+ var _a;
12
+ if (!(reasons === null || reasons === void 0 ? void 0 : reasons.length)) {
13
+ return undefined;
14
+ }
15
+ const texts = [];
16
+ for (const reason of reasons) {
17
+ const text = (_a = getCanonicalPrnReasonText(reason)) === null || _a === void 0 ? void 0 : _a.trim();
18
+ if (!text) {
19
+ continue;
20
+ }
21
+ texts.push(text);
22
+ }
23
+ switch (texts.length) {
24
+ case 0:
25
+ return undefined;
26
+ case 1:
27
+ return texts[0];
28
+ case 2:
29
+ return `${texts[0]} ${conjunction} ${texts[1]}`;
30
+ default: {
31
+ let combined = "";
32
+ for (let index = 0; index < texts.length; index += 1) {
33
+ if (index === 0) {
34
+ combined = texts[index];
35
+ continue;
36
+ }
37
+ if (index === texts.length - 1) {
38
+ combined += ` ${conjunction} ${texts[index]}`;
39
+ continue;
40
+ }
41
+ combined += `, ${texts[index]}`;
42
+ }
43
+ return combined;
44
+ }
45
+ }
46
+ }
47
+ function getPreferredCanonicalPrnReasonText(reason, reasons, conjunction = "or") {
48
+ var _a;
49
+ const direct = (_a = getCanonicalPrnReasonText(reason)) === null || _a === void 0 ? void 0 : _a.trim();
50
+ if (!(reasons === null || reasons === void 0 ? void 0 : reasons.length)) {
51
+ return direct;
52
+ }
53
+ if (!direct) {
54
+ return joinCanonicalPrnReasonTexts(reasons, conjunction);
55
+ }
56
+ return /[,/;]/.test(direct)
57
+ ? joinCanonicalPrnReasonTexts(reasons, conjunction)
58
+ : direct;
59
+ }
package/dist/schedule.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nextDueDoses = nextDueDoses;
4
4
  exports.calculateTotalUnits = calculateTotalUnits;
5
5
  const types_1 = require("./types");
6
+ const advice_1 = require("./advice");
6
7
  const array_1 = require("./utils/array");
7
8
  const units_1 = require("./utils/units");
8
9
  const strength_1 = require("./utils/strength");
@@ -349,6 +350,151 @@ function applyOffset(clock, offsetMinutes) {
349
350
  dayShift
350
351
  };
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
+ }
352
498
  /** Provides the default meal pairing used for AC/PC expansions. */
353
499
  function getDefaultMealPairs(config) {
354
500
  return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner];
@@ -633,6 +779,7 @@ function nextDueDoses(dosage, options) {
633
779
  };
634
780
  const timing = dosage.timing;
635
781
  const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
782
+ const courseEnd = timing && repeat ? resolveRepeatDurationCapEnd(repeat, baseTime, timeZone) : null;
636
783
  if (needsDerivedPriorCount &&
637
784
  orderedAt &&
638
785
  timing &&
@@ -643,6 +790,9 @@ function nextDueDoses(dosage, options) {
643
790
  if (!timing || !repeat) {
644
791
  return [];
645
792
  }
793
+ if (courseEnd && from >= courseEnd) {
794
+ return [];
795
+ }
646
796
  const rawCount = repeat.count;
647
797
  const normalizedCount = rawCount === undefined ? undefined : Math.max(0, Math.floor(rawCount));
648
798
  if (normalizedCount === 0) {
@@ -653,10 +803,21 @@ function nextDueDoses(dosage, options) {
653
803
  return [];
654
804
  }
655
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
+ }
656
816
  const results = [];
657
817
  const seen = new Set();
658
818
  const dayFilter = new Set(((_g = repeat.dayOfWeek) !== null && _g !== void 0 ? _g : []).map((day) => day.toLowerCase()));
659
819
  const enforceDayFilter = dayFilter.size > 0;
820
+ const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
660
821
  const whenCodes = (_h = repeat.when) !== null && _h !== void 0 ? _h : [];
661
822
  const timeOfDayEntries = (_j = repeat.timeOfDay) !== null && _j !== void 0 ? _j : [];
662
823
  if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
@@ -680,7 +841,7 @@ function nextDueDoses(dosage, options) {
680
841
  const includesImmediate = (0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate);
681
842
  if (includesImmediate) {
682
843
  const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
683
- if (!orderedAt || orderedAt >= from) {
844
+ if ((!orderedAt || orderedAt >= from) && (!courseEnd || immediateSource < courseEnd)) {
684
845
  const instantIso = formatZonedIso(immediateSource, timeZone);
685
846
  if (!seen.has(instantIso)) {
686
847
  seen.add(instantIso);
@@ -703,7 +864,9 @@ function nextDueDoses(dosage, options) {
703
864
  let currentDay = startOfLocalDay(from, timeZone);
704
865
  let iterations = 0;
705
866
  const maxIterations = effectiveLimit * 31;
706
- while (results.length < effectiveLimit && iterations < maxIterations) {
867
+ while (results.length < effectiveLimit &&
868
+ iterations < maxIterations &&
869
+ (!courseEnd || currentDay < courseEnd)) {
707
870
  const weekday = getLocalWeekday(currentDay, timeZone);
708
871
  if (!enforceDayFilter || dayFilter.has(weekday)) {
709
872
  for (const entry of expanded) {
@@ -720,6 +883,9 @@ function nextDueDoses(dosage, options) {
720
883
  if (orderedAt && zoned < orderedAt) {
721
884
  continue;
722
885
  }
886
+ if (courseEnd && zoned >= courseEnd) {
887
+ continue;
888
+ }
723
889
  const iso = formatZonedIso(zoned, timeZone);
724
890
  if (!seen.has(iso)) {
725
891
  seen.add(iso);
@@ -748,20 +914,18 @@ function nextDueDoses(dosage, options) {
748
914
  if (treatAsInterval && supportsDayFilteredInterval) {
749
915
  // True interval schedules advance from the order start in fixed units. The
750
916
  // timing.code remains advisory so we only rely on the period/unit fields.
751
- const candidates = generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt);
917
+ const candidates = generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt, courseEnd);
752
918
  return candidates;
753
919
  }
754
- if (enforceDayFilter &&
755
- repeat.period &&
756
- repeat.periodUnit &&
757
- (repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
920
+ if (dayFilteredSeriesRepeat) {
758
921
  return generateDayFilteredPeriodSeries({
759
- repeat,
922
+ repeat: dayFilteredSeriesRepeat,
760
923
  timeZone,
761
924
  dayFilter,
762
925
  anchorDay: startOfLocalDay(baseTime, timeZone),
763
926
  startDay: from,
764
927
  from,
928
+ to: courseEnd !== null && courseEnd !== void 0 ? courseEnd : undefined,
765
929
  orderedAt,
766
930
  limit: effectiveLimit,
767
931
  defaultClock: toLocalClock(baseTime, timeZone)
@@ -778,7 +942,9 @@ function nextDueDoses(dosage, options) {
778
942
  let currentDay = startOfLocalDay(from, timeZone);
779
943
  let iterations = 0;
780
944
  const maxIterations = effectiveLimit * 31;
781
- while (results.length < effectiveLimit && iterations < maxIterations) {
945
+ while (results.length < effectiveLimit &&
946
+ iterations < maxIterations &&
947
+ (!courseEnd || currentDay < courseEnd)) {
782
948
  const weekday = getLocalWeekday(currentDay, timeZone);
783
949
  if (!enforceDayFilter || dayFilter.has(weekday)) {
784
950
  for (const clock of clocks) {
@@ -792,6 +958,9 @@ function nextDueDoses(dosage, options) {
792
958
  if (orderedAt && zoned < orderedAt) {
793
959
  continue;
794
960
  }
961
+ if (courseEnd && zoned >= courseEnd) {
962
+ continue;
963
+ }
795
964
  const iso = formatZonedIso(zoned, timeZone);
796
965
  if (!seen.has(iso)) {
797
966
  seen.add(iso);
@@ -822,6 +991,7 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
822
991
  }
823
992
  const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
824
993
  const enforceDayFilter = dayFilter.size > 0;
994
+ const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
825
995
  const seen = new Set();
826
996
  let count = 0;
827
997
  const recordCandidate = (candidate) => {
@@ -931,12 +1101,9 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
931
1101
  }
932
1102
  return count;
933
1103
  }
934
- if (enforceDayFilter &&
935
- repeat.period &&
936
- repeat.periodUnit &&
937
- (repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
1104
+ if (dayFilteredSeriesRepeat) {
938
1105
  const generated = generateDayFilteredPeriodSeries({
939
- repeat,
1106
+ repeat: dayFilteredSeriesRepeat,
940
1107
  timeZone,
941
1108
  dayFilter,
942
1109
  anchorDay: startOfLocalDay(orderedAt, timeZone),
@@ -983,7 +1150,7 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
983
1150
  * Generates an interval-based series by stepping forward from the base time
984
1151
  * until the requested number of timestamps have been produced.
985
1152
  */
986
- function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt) {
1153
+ function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt, upperBound) {
987
1154
  const increment = createIntervalStepper(repeat, timeZone);
988
1155
  if (!increment) {
989
1156
  return [];
@@ -1001,7 +1168,9 @@ function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone
1001
1168
  current = next;
1002
1169
  guard += 1;
1003
1170
  }
1004
- while (results.length < effectiveLimit && guard < maxIterations) {
1171
+ while (results.length < effectiveLimit &&
1172
+ guard < maxIterations &&
1173
+ (!upperBound || current < upperBound)) {
1005
1174
  const weekday = getLocalWeekday(current, timeZone);
1006
1175
  if (!enforceDayFilter || dayFilter.has(weekday)) {
1007
1176
  if (current < from) {
@@ -1023,6 +1192,9 @@ function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone
1023
1192
  current = next;
1024
1193
  continue;
1025
1194
  }
1195
+ if (upperBound && current >= upperBound) {
1196
+ break;
1197
+ }
1026
1198
  const iso = formatZonedIso(current, timeZone);
1027
1199
  if (!seen.has(iso)) {
1028
1200
  seen.add(iso);
@@ -1214,11 +1386,38 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1214
1386
  const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
1215
1387
  if (!timing || !repeat)
1216
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
+ }
1217
1395
  const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
1218
1396
  const enforceDayFilter = dayFilter.size > 0;
1397
+ const dayFilteredSeriesRepeat = resolveDayFilteredSeriesRepeat(repeat, enforceDayFilter);
1219
1398
  const seen = new Set();
1220
1399
  let count = 0;
1221
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
+ }
1222
1421
  const recordCandidate = (candidate) => {
1223
1422
  if (!candidate)
1224
1423
  return false;
@@ -1266,8 +1465,8 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1266
1465
  return count;
1267
1466
  let currentDay = startOfLocalDay(from, timeZone);
1268
1467
  let iterations = 0;
1269
- const maxIterations = limit !== undefined ? limit * 31 : 365 * 31;
1270
- while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDay < to && iterations < maxIterations) {
1468
+ const maxIterations = hardLimit * 31;
1469
+ while (count < countLimit && currentDay < to && iterations < maxIterations) {
1271
1470
  const weekday = getLocalWeekday(currentDay, timeZone);
1272
1471
  if (!enforceDayFilter || dayFilter.has(weekday)) {
1273
1472
  for (const entry of expanded) {
@@ -1297,7 +1496,7 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1297
1496
  return count;
1298
1497
  let current = baseTime;
1299
1498
  let guard = 0;
1300
- const maxIterations = limit !== undefined ? limit * 1000 : 10000;
1499
+ const maxIterations = hardLimit * 1000;
1301
1500
  // Advance to "from"
1302
1501
  while (current < from && guard < maxIterations) {
1303
1502
  const next = increment(current);
@@ -1306,7 +1505,7 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1306
1505
  current = next;
1307
1506
  guard++;
1308
1507
  }
1309
- while (current < to && count < (limit !== null && limit !== void 0 ? limit : Infinity) && guard < maxIterations) {
1508
+ while (current < to && count < countLimit && guard < maxIterations) {
1310
1509
  const weekday = getLocalWeekday(current, timeZone);
1311
1510
  if (!enforceDayFilter || dayFilter.has(weekday)) {
1312
1511
  recordCandidate(current);
@@ -1325,8 +1524,8 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1325
1524
  return count;
1326
1525
  let currentDay = startOfLocalDay(from, timeZone);
1327
1526
  let iterations = 0;
1328
- const maxIterations = limit !== undefined ? limit * 31 : 365 * 31;
1329
- while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDay < to && iterations < maxIterations) {
1527
+ const maxIterations = hardLimit * 31;
1528
+ while (count < countLimit && currentDay < to && iterations < maxIterations) {
1330
1529
  const weekday = getLocalWeekday(currentDay, timeZone);
1331
1530
  if (!enforceDayFilter || dayFilter.has(weekday)) {
1332
1531
  for (const clock of clocks) {
@@ -1340,12 +1539,9 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1340
1539
  }
1341
1540
  }
1342
1541
  // Fallback for dayOfWeek with period/periodUnit but no explicit frequency/clocks
1343
- if (enforceDayFilter &&
1344
- repeat.period &&
1345
- repeat.periodUnit &&
1346
- (repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
1542
+ if (dayFilteredSeriesRepeat) {
1347
1543
  const generated = generateDayFilteredPeriodSeries({
1348
- repeat,
1544
+ repeat: dayFilteredSeriesRepeat,
1349
1545
  timeZone,
1350
1546
  dayFilter,
1351
1547
  anchorDay: startOfLocalDay(baseTime, timeZone),
@@ -1353,7 +1549,7 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1353
1549
  from,
1354
1550
  to,
1355
1551
  orderedAt,
1356
- limit: limit !== null && limit !== void 0 ? limit : 365 * 31,
1552
+ limit: hardLimit,
1357
1553
  defaultClock: toLocalClock(baseTime, timeZone)
1358
1554
  });
1359
1555
  return count + generated.length;
@@ -1361,9 +1557,10 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
1361
1557
  return count;
1362
1558
  }
1363
1559
  function calculateTotalUnitsSingle(options) {
1364
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
1560
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
1365
1561
  const { dosage, durationValue, durationUnit, roundToMultiple, context } = options;
1366
1562
  const from = coerceDate(options.from, "from");
1563
+ const orderedAtDate = options.orderedAt === undefined ? null : coerceDate(options.orderedAt, "orderedAt");
1367
1564
  const providedConfig = options.config;
1368
1565
  const timeZone = (_a = options.timeZone) !== null && _a !== void 0 ? _a : providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.timeZone;
1369
1566
  if (!timeZone) {
@@ -1388,8 +1585,9 @@ function calculateTotalUnitsSingle(options) {
1388
1585
  else {
1389
1586
  endDay = from;
1390
1587
  }
1391
- const count = countScheduleEvents(dosage, from, endDay, config, options.orderedAt ? coerceDate(options.orderedAt, "orderedAt") : from, options.orderedAt ? coerceDate(options.orderedAt, "orderedAt") : null, 2000);
1392
- const doseQuantity = (_j = (_h = (_g = (_f = dosage.doseAndRate) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.doseQuantity) === null || _h === void 0 ? void 0 : _h.value) !== null && _j !== void 0 ? _j : 0;
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;
1393
1591
  let totalUnits = count * doseQuantity;
1394
1592
  if (roundToMultiple && roundToMultiple > 0) {
1395
1593
  totalUnits = Math.ceil(totalUnits / roundToMultiple) * roundToMultiple;
@@ -1398,7 +1596,7 @@ function calculateTotalUnitsSingle(options) {
1398
1596
  // Handle containers
1399
1597
  const containerValue = context === null || context === void 0 ? void 0 : context.containerValue;
1400
1598
  const containerUnit = context === null || context === void 0 ? void 0 : context.containerUnit;
1401
- const doseUnit = (_m = (_l = (_k = dosage.doseAndRate) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.doseQuantity) === null || _m === void 0 ? void 0 : _m.unit;
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;
1402
1600
  if (containerValue && containerValue > 0) {
1403
1601
  let effectiveUnits = totalUnits;
1404
1602
  if (containerUnit && doseUnit && containerUnit !== doseUnit) {
@@ -0,0 +1,35 @@
1
+ import { Token } from "./parser-state";
2
+ import { BodySiteDefinition, ParseOptions, RouteCode } from "./types";
3
+ export interface SitePhraseServices {
4
+ customSiteHints?: Set<string>;
5
+ siteConnectors: ReadonlySet<string>;
6
+ siteFillerWords: ReadonlySet<string>;
7
+ isInstructionLikeText?: (text: string) => boolean;
8
+ normalizeTokenLower: (token: Token) => string;
9
+ isBodySiteHint: (word: string, customSiteHints?: Set<string>) => boolean;
10
+ hasExplicitSiteIntroduction: (startIndex: number) => boolean;
11
+ isNumericToken: (value: string) => boolean;
12
+ isOrdinalToken: (value: string) => boolean;
13
+ mapFrequencyAdverb: (value: string) => string | undefined;
14
+ mapIntervalUnit: (value: string) => string | undefined;
15
+ normalizeUnit: (value: string, options?: ParseOptions) => string | undefined;
16
+ hasRouteLikeWord: (value: string, options?: ParseOptions) => boolean;
17
+ hasFrequencyLikeWord: (value: string) => boolean;
18
+ getNextActiveToken: (index: number) => Token | undefined;
19
+ getPreviousActiveToken: (index: number) => Token | undefined;
20
+ hasApplicationVerbBefore: (index: number) => boolean;
21
+ }
22
+ export interface SiteLookupServices {
23
+ lookupBodySiteDefinition: (map: Record<string, BodySiteDefinition> | undefined, canonical: string) => BodySiteDefinition | undefined;
24
+ }
25
+ export interface SitePhraseCandidate {
26
+ tokenIndices: number[];
27
+ source: "explicit" | "residual";
28
+ }
29
+ export declare function isTimingOnlySitePhrase(words: string[]): boolean;
30
+ export declare function hasExternalSurfaceModifier(siteText: string): boolean;
31
+ export declare function extractExplicitSiteCandidate(tokens: Token[], consumed: Set<number>, startIndex: number, options: ParseOptions | undefined, services: SitePhraseServices): SitePhraseCandidate | undefined;
32
+ export declare function selectBestResidualSiteCandidate(groups: Array<{
33
+ tokens: Token[];
34
+ }>, prnSiteSuffixIndices: Set<number>, services: SitePhraseServices): SitePhraseCandidate | undefined;
35
+ export declare function inferRouteHintFromSitePhrase(siteText: string, options: ParseOptions | undefined, services: SiteLookupServices): RouteCode | undefined;