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/README.md +101 -1
- package/dist/fhir.js +28 -7
- package/dist/i18n.js +65 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +89 -28
- package/dist/internal-types.d.ts +8 -1
- package/dist/maps.d.ts +8 -1
- package/dist/maps.js +313 -5
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +372 -18
- package/dist/schedule.js +159 -1
- package/dist/types.d.ts +84 -0
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1684
|
-
|
|
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
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
|
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.
|