ezmedicationinput 0.1.11 → 0.1.13
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/internal-types.d.ts +1 -0
- package/dist/parser.js +32 -9
- 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/internal-types.d.ts
CHANGED
package/dist/parser.js
CHANGED
|
@@ -20,6 +20,28 @@ const types_1 = require("./types");
|
|
|
20
20
|
const object_1 = require("./utils/object");
|
|
21
21
|
const array_1 = require("./utils/array");
|
|
22
22
|
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
23
|
+
function buildCustomSiteHints(map) {
|
|
24
|
+
if (!map) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const hints = new Set();
|
|
28
|
+
for (const key of Object.keys(map)) {
|
|
29
|
+
const normalized = (0, maps_1.normalizeBodySiteKey)(key);
|
|
30
|
+
if (!normalized) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
for (const part of normalized.split(" ")) {
|
|
34
|
+
if (part) {
|
|
35
|
+
hints.add(part);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return hints;
|
|
40
|
+
}
|
|
41
|
+
function isBodySiteHint(word, customSiteHints) {
|
|
42
|
+
var _a;
|
|
43
|
+
return BODY_SITE_HINTS.has(word) || ((_a = customSiteHints === null || customSiteHints === void 0 ? void 0 : customSiteHints.has(word)) !== null && _a !== void 0 ? _a : false);
|
|
44
|
+
}
|
|
23
45
|
const BODY_SITE_HINTS = new Set([
|
|
24
46
|
"left",
|
|
25
47
|
"right",
|
|
@@ -362,7 +384,7 @@ function hasBodySiteContextBefore(internal, tokens, index) {
|
|
|
362
384
|
continue;
|
|
363
385
|
}
|
|
364
386
|
const normalized = normalizeTokenLower(token);
|
|
365
|
-
if (
|
|
387
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
366
388
|
return true;
|
|
367
389
|
}
|
|
368
390
|
if (EYE_SITE_TOKENS[normalized]) {
|
|
@@ -399,7 +421,7 @@ function hasBodySiteContextAfter(internal, tokens, index) {
|
|
|
399
421
|
if (SITE_FILLER_WORDS.has(normalized)) {
|
|
400
422
|
continue;
|
|
401
423
|
}
|
|
402
|
-
if (
|
|
424
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
403
425
|
return true;
|
|
404
426
|
}
|
|
405
427
|
if (seenConnector) {
|
|
@@ -481,7 +503,7 @@ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
|
|
|
481
503
|
if (SITE_CONNECTORS.has(normalized)) {
|
|
482
504
|
continue;
|
|
483
505
|
}
|
|
484
|
-
if (
|
|
506
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
485
507
|
return false;
|
|
486
508
|
}
|
|
487
509
|
if (EYE_SITE_TOKENS[normalized]) {
|
|
@@ -1248,7 +1270,8 @@ function parseInternal(input, options) {
|
|
|
1248
1270
|
when: [],
|
|
1249
1271
|
warnings: [],
|
|
1250
1272
|
siteTokenIndices: new Set(),
|
|
1251
|
-
siteLookups: []
|
|
1273
|
+
siteLookups: [],
|
|
1274
|
+
customSiteHints: buildCustomSiteHints(options === null || options === void 0 ? void 0 : options.siteCodeMap)
|
|
1252
1275
|
};
|
|
1253
1276
|
const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
|
|
1254
1277
|
const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
|
|
@@ -1366,7 +1389,7 @@ function parseInternal(input, options) {
|
|
|
1366
1389
|
setRoute(internal, synonym.code, synonym.text);
|
|
1367
1390
|
for (const part of slice) {
|
|
1368
1391
|
mark(internal.consumed, part);
|
|
1369
|
-
if (
|
|
1392
|
+
if (isBodySiteHint(part.lower, internal.customSiteHints)) {
|
|
1370
1393
|
internal.siteTokenIndices.add(part.index);
|
|
1371
1394
|
}
|
|
1372
1395
|
}
|
|
@@ -1710,7 +1733,7 @@ function parseInternal(input, options) {
|
|
|
1710
1733
|
const siteCandidateIndices = new Set();
|
|
1711
1734
|
for (const token of leftoverTokens) {
|
|
1712
1735
|
const normalized = normalizeTokenLower(token);
|
|
1713
|
-
if (
|
|
1736
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
1714
1737
|
siteCandidateIndices.add(token.index);
|
|
1715
1738
|
}
|
|
1716
1739
|
}
|
|
@@ -1728,7 +1751,7 @@ function parseInternal(input, options) {
|
|
|
1728
1751
|
}
|
|
1729
1752
|
const lower = normalizeTokenLower(token);
|
|
1730
1753
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1731
|
-
|
|
1754
|
+
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
1732
1755
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1733
1756
|
indicesToInclude.add(token.index);
|
|
1734
1757
|
prev -= 1;
|
|
@@ -1744,7 +1767,7 @@ function parseInternal(input, options) {
|
|
|
1744
1767
|
}
|
|
1745
1768
|
const lower = normalizeTokenLower(token);
|
|
1746
1769
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1747
|
-
|
|
1770
|
+
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
1748
1771
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1749
1772
|
indicesToInclude.add(token.index);
|
|
1750
1773
|
next += 1;
|
|
@@ -1802,7 +1825,7 @@ function parseInternal(input, options) {
|
|
|
1802
1825
|
const normalizedLower = sanitized.toLowerCase();
|
|
1803
1826
|
const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
|
|
1804
1827
|
const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
|
|
1805
|
-
const hasNonSiteWords = siteWords.some((word) => !
|
|
1828
|
+
const hasNonSiteWords = siteWords.some((word) => !isBodySiteHint(word, internal.customSiteHints));
|
|
1806
1829
|
const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
|
|
1807
1830
|
const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
|
|
1808
1831
|
if (!appliedRouteDescriptor) {
|
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