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 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 effectiveLimit = normalizedCount !== undefined ? Math.min(limit, normalizedCount) : limit;
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
@@ -458,6 +458,7 @@ export interface NextDueDoseOptions {
458
458
  from: Date | string;
459
459
  orderedAt?: Date | string;
460
460
  limit?: number;
461
+ priorCount?: number;
461
462
  timeZone?: string;
462
463
  eventClock?: EventClockMap;
463
464
  mealOffsets?: MealOffsetMap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",