ezmedicationinput 0.1.9 → 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/dist/parser.js CHANGED
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.tokenize = tokenize;
4
13
  exports.parseInternal = parseInternal;
14
+ exports.applySiteCoding = applySiteCoding;
15
+ exports.applySiteCodingAsync = applySiteCodingAsync;
5
16
  const maps_1 = require("./maps");
6
17
  const context_1 = require("./context");
7
18
  const safety_1 = require("./safety");
8
19
  const types_1 = require("./types");
9
20
  const object_1 = require("./utils/object");
10
21
  const array_1 = require("./utils/array");
22
+ const SNOMED_SYSTEM = "http://snomed.info/sct";
11
23
  const BODY_SITE_HINTS = new Set([
12
24
  "left",
13
25
  "right",
@@ -66,10 +78,14 @@ const BODY_SITE_HINTS = new Set([
66
78
  "veins",
67
79
  "vagina",
68
80
  "vaginal",
81
+ "penis",
82
+ "penile",
69
83
  "rectum",
70
84
  "rectal",
71
85
  "anus",
72
- "perineum"
86
+ "perineum",
87
+ "temple",
88
+ "temples"
73
89
  ]);
74
90
  const SITE_CONNECTORS = new Set(["to", "in", "into", "on", "onto", "at"]);
75
91
  const SITE_FILLER_WORDS = new Set([
@@ -241,7 +257,7 @@ const OPHTHALMIC_CONTEXT_TOKENS = new Set([
241
257
  "be"
242
258
  ]);
243
259
  function normalizeTokenLower(token) {
244
- return token.lower.replace(/\./g, "");
260
+ return token.lower.replace(/[.{}]/g, "");
245
261
  }
246
262
  function hasOphthalmicContextHint(tokens, index) {
247
263
  for (let offset = -3; offset <= 3; offset++) {
@@ -605,6 +621,68 @@ function tokenize(input) {
605
621
  }
606
622
  return tokens;
607
623
  }
624
+ /**
625
+ * Locates the span of the detected site tokens within the caller's original
626
+ * input so downstream consumers can highlight or replace the exact substring.
627
+ */
628
+ function computeSiteTextRange(input, tokens, indices) {
629
+ if (!indices.length) {
630
+ return undefined;
631
+ }
632
+ const lowerInput = input.toLowerCase();
633
+ let searchStart = 0;
634
+ let rangeStart;
635
+ let rangeEnd;
636
+ for (const tokenIndex of indices) {
637
+ const token = tokens[tokenIndex];
638
+ if (!token) {
639
+ continue;
640
+ }
641
+ const segment = token.original.trim();
642
+ if (!segment) {
643
+ continue;
644
+ }
645
+ const lowerSegment = segment.toLowerCase();
646
+ const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
647
+ if (foundIndex === -1) {
648
+ return undefined;
649
+ }
650
+ const segmentEnd = foundIndex + lowerSegment.length;
651
+ if (rangeStart === undefined) {
652
+ rangeStart = foundIndex;
653
+ }
654
+ rangeEnd = segmentEnd;
655
+ searchStart = segmentEnd;
656
+ }
657
+ if (rangeStart === undefined || rangeEnd === undefined) {
658
+ return undefined;
659
+ }
660
+ return { start: rangeStart, end: rangeEnd };
661
+ }
662
+ /**
663
+ * Prefers highlighting the sanitized site text when it can be located directly
664
+ * in the original input; otherwise falls back to the broader token-derived
665
+ * range.
666
+ */
667
+ function refineSiteRange(input, sanitized, tokenRange) {
668
+ if (!input) {
669
+ return tokenRange;
670
+ }
671
+ const trimmed = sanitized.trim();
672
+ if (!trimmed) {
673
+ return tokenRange;
674
+ }
675
+ const lowerInput = input.toLowerCase();
676
+ const lowerSanitized = trimmed.toLowerCase();
677
+ let startIndex = tokenRange ? lowerInput.indexOf(lowerSanitized, tokenRange.start) : -1;
678
+ if (startIndex === -1) {
679
+ startIndex = lowerInput.indexOf(lowerSanitized);
680
+ }
681
+ if (startIndex === -1) {
682
+ return tokenRange;
683
+ }
684
+ return { start: startIndex, end: startIndex + lowerSanitized.length };
685
+ }
608
686
  function splitToken(token) {
609
687
  if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
610
688
  return [token];
@@ -1169,7 +1247,8 @@ function parseInternal(input, options) {
1169
1247
  dayOfWeek: [],
1170
1248
  when: [],
1171
1249
  warnings: [],
1172
- siteTokenIndices: new Set()
1250
+ siteTokenIndices: new Set(),
1251
+ siteLookups: []
1173
1252
  };
1174
1253
  const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
1175
1254
  const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
@@ -1630,7 +1709,8 @@ function parseInternal(input, options) {
1630
1709
  const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
1631
1710
  const siteCandidateIndices = new Set();
1632
1711
  for (const token of leftoverTokens) {
1633
- if (BODY_SITE_HINTS.has(token.lower)) {
1712
+ const normalized = normalizeTokenLower(token);
1713
+ if (BODY_SITE_HINTS.has(normalized)) {
1634
1714
  siteCandidateIndices.add(token.index);
1635
1715
  }
1636
1716
  }
@@ -1646,7 +1726,7 @@ function parseInternal(input, options) {
1646
1726
  if (!token) {
1647
1727
  break;
1648
1728
  }
1649
- const lower = token.lower;
1729
+ const lower = normalizeTokenLower(token);
1650
1730
  if (SITE_CONNECTORS.has(lower) ||
1651
1731
  BODY_SITE_HINTS.has(lower) ||
1652
1732
  ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
@@ -1662,7 +1742,7 @@ function parseInternal(input, options) {
1662
1742
  if (!token) {
1663
1743
  break;
1664
1744
  }
1665
- const lower = token.lower;
1745
+ const lower = normalizeTokenLower(token);
1666
1746
  if (SITE_CONNECTORS.has(lower) ||
1667
1747
  BODY_SITE_HINTS.has(lower) ||
1668
1748
  ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
@@ -1680,8 +1760,10 @@ function parseInternal(input, options) {
1680
1760
  if (!token) {
1681
1761
  continue;
1682
1762
  }
1683
- const lower = token.lower;
1684
- if (!SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1763
+ const lower = normalizeTokenLower(token);
1764
+ const trimmed = token.original.trim();
1765
+ const isBraceToken = trimmed.length > 0 && /^[{}]+$/.test(trimmed);
1766
+ if (!isBraceToken && !SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1685
1767
  displayWords.push(token.original);
1686
1768
  }
1687
1769
  mark(internal.consumed, token);
@@ -1691,16 +1773,45 @@ function parseInternal(input, options) {
1691
1773
  .join(" ")
1692
1774
  .trim();
1693
1775
  if (normalizedSite) {
1694
- const normalizedLower = normalizedSite.toLowerCase();
1695
- const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
1696
- const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
1697
- const hasNonSiteWords = siteWords.some((word) => !BODY_SITE_HINTS.has(word));
1698
- const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
1699
- const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(normalizedSite);
1700
- if (!appliedRouteDescriptor) {
1701
- internal.siteText = normalizedSite;
1702
- if (!internal.siteSource) {
1703
- internal.siteSource = "text";
1776
+ const tokenRange = computeSiteTextRange(internal.input, tokens, sortedIndices);
1777
+ let sanitized = normalizedSite;
1778
+ let isProbe = false;
1779
+ const probeMatch = sanitized.match(/^\{(.+)}$/);
1780
+ if (probeMatch) {
1781
+ // `{site}` placeholders flag interactive lookups so consumers can prompt
1782
+ // for a coded selection even when the parser cannot resolve the entry.
1783
+ isProbe = true;
1784
+ sanitized = probeMatch[1];
1785
+ }
1786
+ // Remove stray braces and normalize whitespace so lookups and downstream
1787
+ // displays operate on a clean phrase.
1788
+ sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
1789
+ const range = refineSiteRange(internal.input, sanitized, tokenRange);
1790
+ const sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
1791
+ internal.siteLookupRequest = {
1792
+ originalText: normalizedSite,
1793
+ text: sanitized,
1794
+ normalized: sanitized.toLowerCase(),
1795
+ canonical: sanitized ? (0, maps_1.normalizeBodySiteKey)(sanitized) : "",
1796
+ isProbe,
1797
+ inputText: internal.input,
1798
+ sourceText,
1799
+ range
1800
+ };
1801
+ if (sanitized) {
1802
+ const normalizedLower = sanitized.toLowerCase();
1803
+ const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
1804
+ const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
1805
+ const hasNonSiteWords = siteWords.some((word) => !BODY_SITE_HINTS.has(word));
1806
+ const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
1807
+ const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
1808
+ if (!appliedRouteDescriptor) {
1809
+ // Preserve the clean site text for FHIR output and resolver context
1810
+ // whenever we keep the original phrase.
1811
+ internal.siteText = sanitized;
1812
+ if (!internal.siteSource) {
1813
+ internal.siteSource = "text";
1814
+ }
1704
1815
  }
1705
1816
  }
1706
1817
  }
@@ -1734,6 +1845,249 @@ function parseInternal(input, options) {
1734
1845
  }
1735
1846
  return internal;
1736
1847
  }
1848
+ /**
1849
+ * Resolves parsed site text against SNOMED dictionaries and synchronous
1850
+ * callbacks, applying the best match to the in-progress parse result.
1851
+ */
1852
+ function applySiteCoding(internal, options) {
1853
+ runSiteCodingResolutionSync(internal, options);
1854
+ }
1855
+ /**
1856
+ * Asynchronous counterpart to {@link applySiteCoding} that awaits resolver and
1857
+ * suggestion callbacks so remote terminology services can be used.
1858
+ */
1859
+ function applySiteCodingAsync(internal, options) {
1860
+ return __awaiter(this, void 0, void 0, function* () {
1861
+ yield runSiteCodingResolutionAsync(internal, options);
1862
+ });
1863
+ }
1864
+ /**
1865
+ * Attempts to resolve site codings using built-in dictionaries followed by any
1866
+ * provided synchronous resolvers. Suggestions are collected when resolution
1867
+ * fails or a `{probe}` placeholder requested an interactive lookup.
1868
+ */
1869
+ function runSiteCodingResolutionSync(internal, options) {
1870
+ internal.siteLookups = [];
1871
+ const request = internal.siteLookupRequest;
1872
+ if (!request) {
1873
+ return;
1874
+ }
1875
+ const canonical = request.canonical;
1876
+ const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1877
+ let resolution = customDefinition;
1878
+ if (!resolution) {
1879
+ // Allow synchronous resolver callbacks to claim the site.
1880
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
1881
+ const result = resolver(request);
1882
+ if (isPromise(result)) {
1883
+ throw new Error("Site code resolver returned a Promise; use parseSigAsync for asynchronous site resolution.");
1884
+ }
1885
+ if (result) {
1886
+ resolution = result;
1887
+ break;
1888
+ }
1889
+ }
1890
+ }
1891
+ const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
1892
+ if (!resolution && defaultDefinition) {
1893
+ // Fall back to bundled SNOMED lookups when no overrides claim the site.
1894
+ resolution = defaultDefinition;
1895
+ }
1896
+ if (resolution) {
1897
+ applySiteDefinition(internal, resolution);
1898
+ }
1899
+ else {
1900
+ internal.siteCoding = undefined;
1901
+ }
1902
+ const needsSuggestions = request.isProbe || !resolution;
1903
+ if (!needsSuggestions) {
1904
+ return;
1905
+ }
1906
+ const suggestionMap = new Map();
1907
+ if (customDefinition) {
1908
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1909
+ }
1910
+ if (defaultDefinition) {
1911
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
1912
+ }
1913
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
1914
+ // Aggregates resolver suggestions while guarding against accidental async
1915
+ // usage, mirroring the behavior of site resolvers.
1916
+ const result = resolver(request);
1917
+ if (isPromise(result)) {
1918
+ throw new Error("Site code suggestion resolver returned a Promise; use parseSigAsync for asynchronous site suggestions.");
1919
+ }
1920
+ collectSuggestionResult(suggestionMap, result);
1921
+ }
1922
+ const suggestions = Array.from(suggestionMap.values());
1923
+ if (suggestions.length || request.isProbe) {
1924
+ internal.siteLookups.push({ request, suggestions });
1925
+ }
1926
+ }
1927
+ /**
1928
+ * Async version of {@link runSiteCodingResolutionSync} that awaits resolver
1929
+ * results and suggestion providers, enabling remote terminology services.
1930
+ */
1931
+ function runSiteCodingResolutionAsync(internal, options) {
1932
+ return __awaiter(this, void 0, void 0, function* () {
1933
+ internal.siteLookups = [];
1934
+ const request = internal.siteLookupRequest;
1935
+ if (!request) {
1936
+ return;
1937
+ }
1938
+ const canonical = request.canonical;
1939
+ const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1940
+ let resolution = customDefinition;
1941
+ if (!resolution) {
1942
+ // Await asynchronous resolver callbacks (e.g., HTTP terminology services).
1943
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
1944
+ const result = yield resolver(request);
1945
+ if (result) {
1946
+ resolution = result;
1947
+ break;
1948
+ }
1949
+ }
1950
+ }
1951
+ const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
1952
+ if (!resolution && defaultDefinition) {
1953
+ resolution = defaultDefinition;
1954
+ }
1955
+ if (resolution) {
1956
+ applySiteDefinition(internal, resolution);
1957
+ }
1958
+ else {
1959
+ internal.siteCoding = undefined;
1960
+ }
1961
+ const needsSuggestions = request.isProbe || !resolution;
1962
+ if (!needsSuggestions) {
1963
+ return;
1964
+ }
1965
+ const suggestionMap = new Map();
1966
+ if (customDefinition) {
1967
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1968
+ }
1969
+ if (defaultDefinition) {
1970
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
1971
+ }
1972
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
1973
+ // Async suggestion providers are awaited, allowing UI workflows to fetch
1974
+ // candidate codes on demand.
1975
+ const result = yield resolver(request);
1976
+ collectSuggestionResult(suggestionMap, result);
1977
+ }
1978
+ const suggestions = Array.from(suggestionMap.values());
1979
+ if (suggestions.length || request.isProbe) {
1980
+ internal.siteLookups.push({ request, suggestions });
1981
+ }
1982
+ });
1983
+ }
1984
+ /**
1985
+ * Looks up a body-site definition in a caller-provided map, honoring both
1986
+ * direct keys and entries that normalize to the same canonical phrase.
1987
+ */
1988
+ function lookupBodySiteDefinition(map, canonical) {
1989
+ if (!map) {
1990
+ return undefined;
1991
+ }
1992
+ const direct = map[canonical];
1993
+ if (direct) {
1994
+ return direct;
1995
+ }
1996
+ for (const [key, definition] of (0, object_1.objectEntries)(map)) {
1997
+ if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
1998
+ return definition;
1999
+ }
2000
+ }
2001
+ return undefined;
2002
+ }
2003
+ /**
2004
+ * Applies the selected body-site definition onto the parser state, defaulting
2005
+ * the coding system to SNOMED CT when the definition omits one.
2006
+ */
2007
+ function applySiteDefinition(internal, definition) {
2008
+ var _a;
2009
+ const coding = definition.coding;
2010
+ internal.siteCoding = {
2011
+ code: coding.code,
2012
+ display: coding.display,
2013
+ system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
2014
+ };
2015
+ if (definition.text) {
2016
+ internal.siteText = definition.text;
2017
+ }
2018
+ }
2019
+ /**
2020
+ * Converts a body-site definition into a suggestion payload so all suggestion
2021
+ * sources share consistent structure.
2022
+ */
2023
+ function definitionToSuggestion(definition) {
2024
+ var _a;
2025
+ return {
2026
+ coding: {
2027
+ code: definition.coding.code,
2028
+ display: definition.coding.display,
2029
+ system: (_a = definition.coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
2030
+ },
2031
+ text: definition.text
2032
+ };
2033
+ }
2034
+ /**
2035
+ * Inserts a suggestion into a deduplicated map keyed by system and code.
2036
+ */
2037
+ function addSuggestionToMap(map, suggestion) {
2038
+ var _a, _b;
2039
+ if (!suggestion) {
2040
+ return;
2041
+ }
2042
+ const coding = suggestion.coding;
2043
+ if (!(coding === null || coding === void 0 ? void 0 : coding.code)) {
2044
+ return;
2045
+ }
2046
+ const key = `${(_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM}|${coding.code}`;
2047
+ if (!map.has(key)) {
2048
+ map.set(key, {
2049
+ coding: {
2050
+ code: coding.code,
2051
+ display: coding.display,
2052
+ system: (_b = coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM
2053
+ },
2054
+ text: suggestion.text
2055
+ });
2056
+ }
2057
+ }
2058
+ /**
2059
+ * Normalizes resolver outputs into a consistent array before merging them into
2060
+ * the suggestion map.
2061
+ */
2062
+ function collectSuggestionResult(map, result) {
2063
+ if (!result) {
2064
+ return;
2065
+ }
2066
+ const suggestions = Array.isArray(result)
2067
+ ? result
2068
+ : typeof result === "object" && "suggestions" in result
2069
+ ? result.suggestions
2070
+ : [result];
2071
+ for (const suggestion of suggestions) {
2072
+ addSuggestionToMap(map, suggestion);
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Wraps scalar or array configuration into an array to simplify iteration.
2077
+ */
2078
+ function toArray(value) {
2079
+ if (!value) {
2080
+ return [];
2081
+ }
2082
+ return Array.isArray(value) ? value : [value];
2083
+ }
2084
+ /**
2085
+ * Detects thenables without relying on `instanceof Promise`, which can break
2086
+ * across execution contexts.
2087
+ */
2088
+ function isPromise(value) {
2089
+ return !!value && typeof value.then === "function";
2090
+ }
1737
2091
  function normalizeUnit(token, options) {
1738
2092
  var _a;
1739
2093
  const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
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.