ezmedicationinput 0.1.11 → 0.1.12
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/README.md +1 -1
- package/dist/schedule.js +159 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ You can specify the number of times (total count) the medication is supposed to
|
|
|
228
228
|
|
|
229
229
|
### Next due dose generation
|
|
230
230
|
|
|
231
|
-
`nextDueDoses` produces upcoming administration timestamps from an existing FHIR `Dosage`. Supply the evaluation window (`from`), optionally the order start (`orderedAt`), and clinic clock details such as a time zone and event timing anchors.
|
|
231
|
+
`nextDueDoses` produces upcoming administration timestamps from an existing FHIR `Dosage`. Supply the evaluation window (`from`), optionally the order start (`orderedAt`), and clinic clock details such as a time zone and event timing anchors. When a `Timing.repeat.count` cap exists and prior occurrences have already been administered, pass `priorCount` to indicate how many doses were consumed before the `from` timestamp so remaining administrations are calculated correctly without re-traversing the timeline.
|
|
232
232
|
|
|
233
233
|
```ts
|
|
234
234
|
import { EventTiming, nextDueDoses, parseSig } from "ezmedicationinput";
|
package/dist/schedule.js
CHANGED
|
@@ -475,6 +475,14 @@ function nextDueDoses(dosage, options) {
|
|
|
475
475
|
}
|
|
476
476
|
const from = coerceDate(options.from, "from");
|
|
477
477
|
const orderedAt = options.orderedAt === undefined ? null : coerceDate(options.orderedAt, "orderedAt");
|
|
478
|
+
const priorCountInput = options.priorCount;
|
|
479
|
+
if (priorCountInput !== undefined) {
|
|
480
|
+
if (!Number.isFinite(priorCountInput) || priorCountInput < 0) {
|
|
481
|
+
throw new Error("Invalid priorCount supplied to nextDueDoses");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
let priorCount = priorCountInput !== undefined ? Math.floor(priorCountInput) : 0;
|
|
485
|
+
const needsDerivedPriorCount = priorCountInput === undefined && !!orderedAt;
|
|
478
486
|
const baseTime = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
|
|
479
487
|
const providedConfig = options.config;
|
|
480
488
|
const timeZone = (_b = options.timeZone) !== null && _b !== void 0 ? _b : providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.timeZone;
|
|
@@ -492,6 +500,13 @@ function nextDueDoses(dosage, options) {
|
|
|
492
500
|
};
|
|
493
501
|
const timing = dosage.timing;
|
|
494
502
|
const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
|
|
503
|
+
if (needsDerivedPriorCount &&
|
|
504
|
+
orderedAt &&
|
|
505
|
+
timing &&
|
|
506
|
+
repeat &&
|
|
507
|
+
repeat.count !== undefined) {
|
|
508
|
+
priorCount = derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, timeZone);
|
|
509
|
+
}
|
|
495
510
|
if (!timing || !repeat) {
|
|
496
511
|
return [];
|
|
497
512
|
}
|
|
@@ -500,7 +515,11 @@ function nextDueDoses(dosage, options) {
|
|
|
500
515
|
if (normalizedCount === 0) {
|
|
501
516
|
return [];
|
|
502
517
|
}
|
|
503
|
-
const
|
|
518
|
+
const remainingCount = normalizedCount === undefined ? undefined : Math.max(0, normalizedCount - priorCount);
|
|
519
|
+
if (remainingCount === 0) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
const effectiveLimit = remainingCount !== undefined ? Math.min(limit, remainingCount) : limit;
|
|
504
523
|
const results = [];
|
|
505
524
|
const seen = new Set();
|
|
506
525
|
const dayFilter = new Set(((_g = repeat.dayOfWeek) !== null && _g !== void 0 ? _g : []).map((day) => day.toLowerCase()));
|
|
@@ -628,6 +647,145 @@ function nextDueDoses(dosage, options) {
|
|
|
628
647
|
}
|
|
629
648
|
return [];
|
|
630
649
|
}
|
|
650
|
+
function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, timeZone) {
|
|
651
|
+
var _a, _b, _c;
|
|
652
|
+
if (from <= orderedAt) {
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
const normalizedCount = repeat.count === undefined
|
|
656
|
+
? undefined
|
|
657
|
+
: Math.max(0, Math.floor(repeat.count));
|
|
658
|
+
if (normalizedCount === 0) {
|
|
659
|
+
return 0;
|
|
660
|
+
}
|
|
661
|
+
const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
|
|
662
|
+
const enforceDayFilter = dayFilter.size > 0;
|
|
663
|
+
const seen = new Set();
|
|
664
|
+
let count = 0;
|
|
665
|
+
const recordCandidate = (candidate) => {
|
|
666
|
+
if (!candidate) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (candidate < orderedAt || candidate >= from) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
const iso = formatZonedIso(candidate, timeZone);
|
|
673
|
+
if (seen.has(iso)) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
seen.add(iso);
|
|
677
|
+
count += 1;
|
|
678
|
+
return true;
|
|
679
|
+
};
|
|
680
|
+
const whenCodes = (_b = repeat.when) !== null && _b !== void 0 ? _b : [];
|
|
681
|
+
const timeOfDayEntries = (_c = repeat.timeOfDay) !== null && _c !== void 0 ? _c : [];
|
|
682
|
+
if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
|
|
683
|
+
const expanded = expandWhenCodes(whenCodes, config, repeat);
|
|
684
|
+
if (timeOfDayEntries.length > 0) {
|
|
685
|
+
for (const clock of timeOfDayEntries) {
|
|
686
|
+
expanded.push({ time: normalizeClock(clock), dayShift: 0 });
|
|
687
|
+
}
|
|
688
|
+
expanded.sort((a, b) => {
|
|
689
|
+
if (a.dayShift !== b.dayShift) {
|
|
690
|
+
return a.dayShift - b.dayShift;
|
|
691
|
+
}
|
|
692
|
+
return a.time.localeCompare(b.time);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
|
|
696
|
+
if (recordCandidate(orderedAt) && normalizedCount !== undefined && seen.size >= normalizedCount) {
|
|
697
|
+
return count;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (expanded.length === 0) {
|
|
701
|
+
return count;
|
|
702
|
+
}
|
|
703
|
+
let currentDay = startOfLocalDay(orderedAt, timeZone);
|
|
704
|
+
let iterations = 0;
|
|
705
|
+
const maxIterations = normalizedCount !== undefined ? normalizedCount * 31 : 31 * 365;
|
|
706
|
+
while (currentDay < from && iterations < maxIterations) {
|
|
707
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
708
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
709
|
+
for (const entry of expanded) {
|
|
710
|
+
const targetDay = entry.dayShift === 0
|
|
711
|
+
? currentDay
|
|
712
|
+
: addLocalDays(currentDay, entry.dayShift, timeZone);
|
|
713
|
+
const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
|
|
714
|
+
if (!zoned) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (zoned < orderedAt || zoned >= from) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (recordCandidate(zoned) && normalizedCount !== undefined && seen.size >= normalizedCount) {
|
|
721
|
+
return count;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
726
|
+
iterations += 1;
|
|
727
|
+
}
|
|
728
|
+
return count;
|
|
729
|
+
}
|
|
730
|
+
const treatAsInterval = !!repeat.period &&
|
|
731
|
+
!!repeat.periodUnit &&
|
|
732
|
+
(!repeat.frequency ||
|
|
733
|
+
repeat.periodUnit !== "d" ||
|
|
734
|
+
(repeat.frequency === 1 && repeat.period > 1));
|
|
735
|
+
if (treatAsInterval) {
|
|
736
|
+
const increment = createIntervalStepper(repeat, timeZone);
|
|
737
|
+
if (!increment) {
|
|
738
|
+
return count;
|
|
739
|
+
}
|
|
740
|
+
let current = orderedAt;
|
|
741
|
+
let guard = 0;
|
|
742
|
+
const maxIterations = normalizedCount !== undefined ? normalizedCount * 1000 : 1000;
|
|
743
|
+
while (current < from && guard < maxIterations) {
|
|
744
|
+
const weekday = getLocalWeekday(current, timeZone);
|
|
745
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
746
|
+
if (recordCandidate(current) && normalizedCount !== undefined && seen.size >= normalizedCount) {
|
|
747
|
+
return count;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const next = increment(current);
|
|
751
|
+
if (!next || next.getTime() === current.getTime()) {
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
current = next;
|
|
755
|
+
guard += 1;
|
|
756
|
+
}
|
|
757
|
+
return count;
|
|
758
|
+
}
|
|
759
|
+
if (repeat.frequency && repeat.period && repeat.periodUnit) {
|
|
760
|
+
const clocks = resolveFrequencyClocks(timing, config);
|
|
761
|
+
if (clocks.length === 0) {
|
|
762
|
+
return count;
|
|
763
|
+
}
|
|
764
|
+
let currentDay = startOfLocalDay(orderedAt, timeZone);
|
|
765
|
+
let iterations = 0;
|
|
766
|
+
const maxIterations = normalizedCount !== undefined ? normalizedCount * 31 : 31 * 365;
|
|
767
|
+
while (currentDay < from && iterations < maxIterations) {
|
|
768
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
769
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
770
|
+
for (const clock of clocks) {
|
|
771
|
+
const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
|
|
772
|
+
if (!zoned) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
if (zoned < orderedAt || zoned >= from) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (recordCandidate(zoned) && normalizedCount !== undefined && seen.size >= normalizedCount) {
|
|
779
|
+
return count;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
784
|
+
iterations += 1;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return count;
|
|
788
|
+
}
|
|
631
789
|
/**
|
|
632
790
|
* Generates an interval-based series by stepping forward from the base time
|
|
633
791
|
* until the requested number of timestamps have been produced.
|
package/dist/types.d.ts
CHANGED