ezmedicationinput 0.1.7 → 0.1.9
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 +2 -0
- package/dist/fhir.js +17 -12
- package/dist/format.js +9 -0
- package/dist/i18n.js +9 -0
- package/dist/internal-types.d.ts +1 -0
- package/dist/parser.js +248 -5
- package/dist/schedule.js +30 -16
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ When `when` is populated, `timeOfDay` is intentionally omitted to stay within HL
|
|
|
110
110
|
|
|
111
111
|
Routes always include SNOMED CT codings. Every code from the SNOMED Route of Administration value set is represented so you can confidently pass parsed results into downstream FHIR services that expect coded routes.
|
|
112
112
|
|
|
113
|
+
You can specify the number of times (total count) the medication is supposed to be used by ending with `for {number} times`, `x {number} doses`, or simply `x {number}`
|
|
114
|
+
|
|
113
115
|
### Advanced parsing options
|
|
114
116
|
|
|
115
117
|
`parseSig` accepts a `ParseOptions` object. Highlights:
|
package/dist/fhir.js
CHANGED
|
@@ -17,6 +17,10 @@ function toFhir(internal) {
|
|
|
17
17
|
repeat.frequency = internal.frequency;
|
|
18
18
|
hasRepeat = true;
|
|
19
19
|
}
|
|
20
|
+
if (internal.count !== undefined) {
|
|
21
|
+
repeat.count = internal.count;
|
|
22
|
+
hasRepeat = true;
|
|
23
|
+
}
|
|
20
24
|
if (internal.frequencyMax !== undefined) {
|
|
21
25
|
repeat.frequencyMax = internal.frequencyMax;
|
|
22
26
|
hasRepeat = true;
|
|
@@ -112,7 +116,7 @@ function toFhir(internal) {
|
|
|
112
116
|
return dosage;
|
|
113
117
|
}
|
|
114
118
|
function internalFromFhir(dosage) {
|
|
115
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
|
|
119
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
|
|
116
120
|
const internal = {
|
|
117
121
|
input: (_a = dosage.text) !== null && _a !== void 0 ? _a : "",
|
|
118
122
|
tokens: [],
|
|
@@ -125,18 +129,19 @@ function internalFromFhir(dosage) {
|
|
|
125
129
|
: [],
|
|
126
130
|
warnings: [],
|
|
127
131
|
timingCode: (_j = (_h = (_g = (_f = dosage.timing) === null || _f === void 0 ? void 0 : _f.code) === null || _g === void 0 ? void 0 : _g.coding) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.code,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
count: (_l = (_k = dosage.timing) === null || _k === void 0 ? void 0 : _k.repeat) === null || _l === void 0 ? void 0 : _l.count,
|
|
133
|
+
frequency: (_o = (_m = dosage.timing) === null || _m === void 0 ? void 0 : _m.repeat) === null || _o === void 0 ? void 0 : _o.frequency,
|
|
134
|
+
frequencyMax: (_q = (_p = dosage.timing) === null || _p === void 0 ? void 0 : _p.repeat) === null || _q === void 0 ? void 0 : _q.frequencyMax,
|
|
135
|
+
period: (_s = (_r = dosage.timing) === null || _r === void 0 ? void 0 : _r.repeat) === null || _s === void 0 ? void 0 : _s.period,
|
|
136
|
+
periodMax: (_u = (_t = dosage.timing) === null || _t === void 0 ? void 0 : _t.repeat) === null || _u === void 0 ? void 0 : _u.periodMax,
|
|
137
|
+
periodUnit: (_w = (_v = dosage.timing) === null || _v === void 0 ? void 0 : _v.repeat) === null || _w === void 0 ? void 0 : _w.periodUnit,
|
|
138
|
+
routeText: (_x = dosage.route) === null || _x === void 0 ? void 0 : _x.text,
|
|
139
|
+
siteText: (_y = dosage.site) === null || _y === void 0 ? void 0 : _y.text,
|
|
135
140
|
asNeeded: dosage.asNeededBoolean,
|
|
136
|
-
asNeededReason: (
|
|
141
|
+
asNeededReason: (_0 = (_z = dosage.asNeededFor) === null || _z === void 0 ? void 0 : _z[0]) === null || _0 === void 0 ? void 0 : _0.text,
|
|
137
142
|
siteTokenIndices: new Set()
|
|
138
143
|
};
|
|
139
|
-
const routeCoding = (
|
|
144
|
+
const routeCoding = (_2 = (_1 = dosage.route) === null || _1 === void 0 ? void 0 : _1.coding) === null || _2 === void 0 ? void 0 : _2.find((code) => code.system === SNOMED_SYSTEM);
|
|
140
145
|
if (routeCoding === null || routeCoding === void 0 ? void 0 : routeCoding.code) {
|
|
141
146
|
// Translate SNOMED codings back into the simplified enum for round-trip fidelity.
|
|
142
147
|
const mapped = maps_1.ROUTE_BY_SNOMED[routeCoding.code];
|
|
@@ -145,13 +150,13 @@ function internalFromFhir(dosage) {
|
|
|
145
150
|
internal.routeText = maps_1.ROUTE_TEXT[mapped];
|
|
146
151
|
}
|
|
147
152
|
}
|
|
148
|
-
const doseAndRate = (
|
|
153
|
+
const doseAndRate = (_3 = dosage.doseAndRate) === null || _3 === void 0 ? void 0 : _3[0];
|
|
149
154
|
if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseRange) {
|
|
150
155
|
const { low, high } = doseAndRate.doseRange;
|
|
151
156
|
if ((low === null || low === void 0 ? void 0 : low.value) !== undefined && (high === null || high === void 0 ? void 0 : high.value) !== undefined) {
|
|
152
157
|
internal.doseRange = { low: low.value, high: high.value };
|
|
153
158
|
}
|
|
154
|
-
internal.unit = (
|
|
159
|
+
internal.unit = (_5 = (_4 = low === null || low === void 0 ? void 0 : low.unit) !== null && _4 !== void 0 ? _4 : high === null || high === void 0 ? void 0 : high.unit) !== null && _5 !== void 0 ? _5 : internal.unit;
|
|
155
160
|
}
|
|
156
161
|
else if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseQuantity) {
|
|
157
162
|
const dose = doseAndRate.doseQuantity;
|
package/dist/format.js
CHANGED
|
@@ -548,6 +548,9 @@ function formatShort(internal) {
|
|
|
548
548
|
.map((d) => d.charAt(0).toUpperCase() + d.slice(1, 3))
|
|
549
549
|
.join(","));
|
|
550
550
|
}
|
|
551
|
+
if (internal.count !== undefined) {
|
|
552
|
+
parts.push(`x${stripTrailingZero(internal.count)}`);
|
|
553
|
+
}
|
|
551
554
|
if (internal.asNeeded) {
|
|
552
555
|
if (internal.asNeededReason) {
|
|
553
556
|
parts.push(`PRN ${internal.asNeededReason}`);
|
|
@@ -568,6 +571,9 @@ function formatLong(internal) {
|
|
|
568
571
|
const eventParts = collectWhenPhrases(internal);
|
|
569
572
|
const timing = combineFrequencyAndEvents(frequencyPart, eventParts);
|
|
570
573
|
const dayPart = describeDayOfWeek(internal);
|
|
574
|
+
const countPart = internal.count
|
|
575
|
+
? `for ${stripTrailingZero(internal.count)} ${internal.count === 1 ? "dose" : "doses"}`
|
|
576
|
+
: undefined;
|
|
571
577
|
const asNeededPart = internal.asNeeded
|
|
572
578
|
? internal.asNeededReason
|
|
573
579
|
? `as needed for ${internal.asNeededReason}`
|
|
@@ -586,6 +592,9 @@ function formatLong(internal) {
|
|
|
586
592
|
if (dayPart) {
|
|
587
593
|
segments.push(dayPart);
|
|
588
594
|
}
|
|
595
|
+
if (countPart) {
|
|
596
|
+
segments.push(countPart);
|
|
597
|
+
}
|
|
589
598
|
if (asNeededPart) {
|
|
590
599
|
segments.push(asNeededPart);
|
|
591
600
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -613,6 +613,9 @@ function formatShortThai(internal) {
|
|
|
613
613
|
.join(",");
|
|
614
614
|
parts.push(days);
|
|
615
615
|
}
|
|
616
|
+
if (internal.count !== undefined) {
|
|
617
|
+
parts.push(`x${stripTrailingZero(internal.count)}`);
|
|
618
|
+
}
|
|
616
619
|
const asNeeded = formatAsNeededThai(internal);
|
|
617
620
|
if (asNeeded) {
|
|
618
621
|
parts.push(asNeeded);
|
|
@@ -629,6 +632,9 @@ function formatLongThai(internal) {
|
|
|
629
632
|
const eventParts = collectWhenPhrasesThai(internal);
|
|
630
633
|
const timing = combineFrequencyAndEventsThai(frequencyPart, eventParts);
|
|
631
634
|
const dayPart = describeDayOfWeekThai(internal);
|
|
635
|
+
const countPart = internal.count !== undefined
|
|
636
|
+
? `จำนวน ${stripTrailingZero(internal.count)} ครั้ง`
|
|
637
|
+
: undefined;
|
|
632
638
|
const asNeeded = formatAsNeededThai(internal);
|
|
633
639
|
const segments = [dosePart];
|
|
634
640
|
if (routePart) {
|
|
@@ -643,6 +649,9 @@ function formatLongThai(internal) {
|
|
|
643
649
|
if (dayPart) {
|
|
644
650
|
segments.push(dayPart);
|
|
645
651
|
}
|
|
652
|
+
if (countPart) {
|
|
653
|
+
segments.push(countPart);
|
|
654
|
+
}
|
|
646
655
|
if (asNeeded) {
|
|
647
656
|
segments.push(asNeeded);
|
|
648
657
|
}
|
package/dist/internal-types.d.ts
CHANGED
package/dist/parser.js
CHANGED
|
@@ -111,6 +111,58 @@ const COMBO_EVENT_TIMINGS = {
|
|
|
111
111
|
"upon waking": types_1.EventTiming.Wake
|
|
112
112
|
};
|
|
113
113
|
const MEAL_CONTEXT_CONNECTORS = new Set(["and", "or", "&", "+", "plus"]);
|
|
114
|
+
const COUNT_KEYWORDS = new Set([
|
|
115
|
+
"time",
|
|
116
|
+
"times",
|
|
117
|
+
"dose",
|
|
118
|
+
"doses",
|
|
119
|
+
"application",
|
|
120
|
+
"applications",
|
|
121
|
+
"use",
|
|
122
|
+
"uses"
|
|
123
|
+
]);
|
|
124
|
+
const COUNT_CONNECTOR_WORDS = new Set([
|
|
125
|
+
"a",
|
|
126
|
+
"an",
|
|
127
|
+
"the",
|
|
128
|
+
"total",
|
|
129
|
+
"of",
|
|
130
|
+
"up",
|
|
131
|
+
"to",
|
|
132
|
+
"no",
|
|
133
|
+
"more",
|
|
134
|
+
"than",
|
|
135
|
+
"max",
|
|
136
|
+
"maximum",
|
|
137
|
+
"additional",
|
|
138
|
+
"extra"
|
|
139
|
+
]);
|
|
140
|
+
const ROUTE_DESCRIPTOR_FILLER_WORDS = new Set([
|
|
141
|
+
"per",
|
|
142
|
+
"by",
|
|
143
|
+
"via",
|
|
144
|
+
"the",
|
|
145
|
+
"a",
|
|
146
|
+
"an"
|
|
147
|
+
]);
|
|
148
|
+
function normalizeRouteDescriptorPhrase(phrase) {
|
|
149
|
+
return phrase
|
|
150
|
+
.trim()
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.split(/\s+/)
|
|
153
|
+
.filter((word) => word.length > 0 && !ROUTE_DESCRIPTOR_FILLER_WORDS.has(word))
|
|
154
|
+
.join(" ");
|
|
155
|
+
}
|
|
156
|
+
const DEFAULT_ROUTE_DESCRIPTOR_SYNONYMS = (() => {
|
|
157
|
+
const map = new Map();
|
|
158
|
+
for (const [phrase, synonym] of (0, object_1.objectEntries)(maps_1.DEFAULT_ROUTE_SYNONYMS)) {
|
|
159
|
+
const normalized = normalizeRouteDescriptorPhrase(phrase);
|
|
160
|
+
if (normalized && !map.has(normalized)) {
|
|
161
|
+
map.set(normalized, synonym);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return map;
|
|
165
|
+
})();
|
|
114
166
|
// Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
|
|
115
167
|
// logic bail early when the clinician already specified precise events.
|
|
116
168
|
const SPECIFIC_MEAL_TIMINGS = new Set([
|
|
@@ -1093,6 +1145,20 @@ function parseNumericRange(token) {
|
|
|
1093
1145
|
}
|
|
1094
1146
|
return { low, high };
|
|
1095
1147
|
}
|
|
1148
|
+
function applyCountLimit(internal, value) {
|
|
1149
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
if (internal.count !== undefined) {
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
const rounded = Math.round(value);
|
|
1156
|
+
if (rounded <= 0) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
internal.count = rounded;
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1096
1162
|
function parseInternal(input, options) {
|
|
1097
1163
|
var _a, _b, _c, _d, _e, _f;
|
|
1098
1164
|
const tokens = tokenize(input);
|
|
@@ -1112,6 +1178,11 @@ function parseInternal(input, options) {
|
|
|
1112
1178
|
value
|
|
1113
1179
|
]))
|
|
1114
1180
|
: undefined;
|
|
1181
|
+
const customRouteDescriptorMap = customRouteMap
|
|
1182
|
+
? new Map(Array.from(customRouteMap.entries())
|
|
1183
|
+
.map(([key, value]) => [normalizeRouteDescriptorPhrase(key), value])
|
|
1184
|
+
.filter(([normalized]) => normalized.length > 0))
|
|
1185
|
+
: undefined;
|
|
1115
1186
|
if (tokens.length === 0) {
|
|
1116
1187
|
return internal;
|
|
1117
1188
|
}
|
|
@@ -1155,6 +1226,50 @@ function parseInternal(input, options) {
|
|
|
1155
1226
|
mark(internal.consumed, token);
|
|
1156
1227
|
}
|
|
1157
1228
|
}
|
|
1229
|
+
const applyRouteDescriptor = (code, text) => {
|
|
1230
|
+
if (internal.routeCode && internal.routeCode !== code) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
setRoute(internal, code, text);
|
|
1234
|
+
return true;
|
|
1235
|
+
};
|
|
1236
|
+
const maybeApplyRouteDescriptor = (phrase) => {
|
|
1237
|
+
if (!phrase) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
const normalized = phrase.trim().toLowerCase();
|
|
1241
|
+
if (!normalized) {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
const customCode = customRouteMap === null || customRouteMap === void 0 ? void 0 : customRouteMap.get(normalized);
|
|
1245
|
+
if (customCode) {
|
|
1246
|
+
if (applyRouteDescriptor(customCode)) {
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const synonym = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized];
|
|
1251
|
+
if (synonym) {
|
|
1252
|
+
if (applyRouteDescriptor(synonym.code, synonym.text)) {
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const normalizedDescriptor = normalizeRouteDescriptorPhrase(normalized);
|
|
1257
|
+
if (normalizedDescriptor && normalizedDescriptor !== normalized) {
|
|
1258
|
+
const customDescriptorCode = customRouteDescriptorMap === null || customRouteDescriptorMap === void 0 ? void 0 : customRouteDescriptorMap.get(normalizedDescriptor);
|
|
1259
|
+
if (customDescriptorCode) {
|
|
1260
|
+
if (applyRouteDescriptor(customDescriptorCode)) {
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const fallbackSynonym = DEFAULT_ROUTE_DESCRIPTOR_SYNONYMS.get(normalizedDescriptor);
|
|
1265
|
+
if (fallbackSynonym) {
|
|
1266
|
+
if (applyRouteDescriptor(fallbackSynonym.code, fallbackSynonym.text)) {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return false;
|
|
1272
|
+
};
|
|
1158
1273
|
// Process tokens sequentially
|
|
1159
1274
|
const tryRouteSynonym = (startIndex) => {
|
|
1160
1275
|
const maxSpan = Math.min(24, tokens.length - startIndex);
|
|
@@ -1275,6 +1390,122 @@ function parseInternal(input, options) {
|
|
|
1275
1390
|
mark(internal.consumed, token);
|
|
1276
1391
|
continue;
|
|
1277
1392
|
}
|
|
1393
|
+
if (internal.count === undefined) {
|
|
1394
|
+
const countMatch = token.lower.match(/^[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
1395
|
+
if (countMatch) {
|
|
1396
|
+
if (applyCountLimit(internal, parseFloat(countMatch[1]))) {
|
|
1397
|
+
mark(internal.consumed, token);
|
|
1398
|
+
const nextToken = tokens[i + 1];
|
|
1399
|
+
if (nextToken && COUNT_KEYWORDS.has(nextToken.lower)) {
|
|
1400
|
+
mark(internal.consumed, nextToken);
|
|
1401
|
+
}
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (token.lower === "x" || token.lower === "*") {
|
|
1406
|
+
const numericToken = tokens[i + 1];
|
|
1407
|
+
if (numericToken &&
|
|
1408
|
+
!internal.consumed.has(numericToken.index) &&
|
|
1409
|
+
/^[0-9]+(?:\.[0-9]+)?$/.test(numericToken.lower) &&
|
|
1410
|
+
applyCountLimit(internal, parseFloat(numericToken.original))) {
|
|
1411
|
+
mark(internal.consumed, token);
|
|
1412
|
+
mark(internal.consumed, numericToken);
|
|
1413
|
+
const afterToken = tokens[i + 2];
|
|
1414
|
+
if (afterToken && COUNT_KEYWORDS.has(afterToken.lower)) {
|
|
1415
|
+
mark(internal.consumed, afterToken);
|
|
1416
|
+
}
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (token.lower === "for") {
|
|
1421
|
+
const skipConnectors = (startIndex, bucket) => {
|
|
1422
|
+
let cursor = startIndex;
|
|
1423
|
+
while (cursor < tokens.length) {
|
|
1424
|
+
const candidate = tokens[cursor];
|
|
1425
|
+
if (!candidate) {
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
if (internal.consumed.has(candidate.index)) {
|
|
1429
|
+
cursor += 1;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (!COUNT_CONNECTOR_WORDS.has(candidate.lower)) {
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
bucket.push(candidate);
|
|
1436
|
+
cursor += 1;
|
|
1437
|
+
}
|
|
1438
|
+
return cursor;
|
|
1439
|
+
};
|
|
1440
|
+
const preConnectors = [];
|
|
1441
|
+
let lookaheadIndex = skipConnectors(i + 1, preConnectors);
|
|
1442
|
+
const numericToken = tokens[lookaheadIndex];
|
|
1443
|
+
if (numericToken &&
|
|
1444
|
+
!internal.consumed.has(numericToken.index) &&
|
|
1445
|
+
/^[0-9]+(?:\.[0-9]+)?$/.test(numericToken.lower)) {
|
|
1446
|
+
const postConnectors = [];
|
|
1447
|
+
lookaheadIndex = skipConnectors(lookaheadIndex + 1, postConnectors);
|
|
1448
|
+
const keywordToken = tokens[lookaheadIndex];
|
|
1449
|
+
if (keywordToken &&
|
|
1450
|
+
!internal.consumed.has(keywordToken.index) &&
|
|
1451
|
+
COUNT_KEYWORDS.has(keywordToken.lower) &&
|
|
1452
|
+
applyCountLimit(internal, parseFloat(numericToken.original))) {
|
|
1453
|
+
mark(internal.consumed, token);
|
|
1454
|
+
for (const connector of preConnectors) {
|
|
1455
|
+
mark(internal.consumed, connector);
|
|
1456
|
+
}
|
|
1457
|
+
mark(internal.consumed, numericToken);
|
|
1458
|
+
for (const connector of postConnectors) {
|
|
1459
|
+
mark(internal.consumed, connector);
|
|
1460
|
+
}
|
|
1461
|
+
mark(internal.consumed, keywordToken);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (COUNT_KEYWORDS.has(token.lower)) {
|
|
1467
|
+
const partsToMark = [token];
|
|
1468
|
+
let value;
|
|
1469
|
+
const prevToken = tokens[i - 1];
|
|
1470
|
+
if (prevToken && !internal.consumed.has(prevToken.index)) {
|
|
1471
|
+
const prevLower = prevToken.lower;
|
|
1472
|
+
const suffixMatch = prevLower.match(/^([0-9]+(?:\.[0-9]+)?)[x*]$/);
|
|
1473
|
+
const prefixMatch = prevLower.match(/^[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
1474
|
+
if (suffixMatch) {
|
|
1475
|
+
value = parseFloat(suffixMatch[1]);
|
|
1476
|
+
partsToMark.push(prevToken);
|
|
1477
|
+
}
|
|
1478
|
+
else if (prefixMatch) {
|
|
1479
|
+
value = parseFloat(prefixMatch[1]);
|
|
1480
|
+
partsToMark.push(prevToken);
|
|
1481
|
+
}
|
|
1482
|
+
else if (/^[0-9]+(?:\.[0-9]+)?$/.test(prevLower)) {
|
|
1483
|
+
const maybeX = tokens[i - 2];
|
|
1484
|
+
if (maybeX &&
|
|
1485
|
+
!internal.consumed.has(maybeX.index) &&
|
|
1486
|
+
(maybeX.lower === "x" || maybeX.lower === "*")) {
|
|
1487
|
+
value = parseFloat(prevToken.original);
|
|
1488
|
+
partsToMark.push(maybeX, prevToken);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (value === undefined) {
|
|
1493
|
+
const nextToken = tokens[i + 1];
|
|
1494
|
+
if (nextToken &&
|
|
1495
|
+
!internal.consumed.has(nextToken.index) &&
|
|
1496
|
+
/^[0-9]+(?:\.[0-9]+)?$/.test(nextToken.lower)) {
|
|
1497
|
+
value = parseFloat(nextToken.original);
|
|
1498
|
+
partsToMark.push(nextToken);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (applyCountLimit(internal, value)) {
|
|
1502
|
+
for (const part of partsToMark) {
|
|
1503
|
+
mark(internal.consumed, part);
|
|
1504
|
+
}
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1278
1509
|
// Numeric dose
|
|
1279
1510
|
const rangeValue = parseNumericRange(token.lower);
|
|
1280
1511
|
if (rangeValue) {
|
|
@@ -1416,7 +1647,9 @@ function parseInternal(input, options) {
|
|
|
1416
1647
|
break;
|
|
1417
1648
|
}
|
|
1418
1649
|
const lower = token.lower;
|
|
1419
|
-
if (SITE_CONNECTORS.has(lower) ||
|
|
1650
|
+
if (SITE_CONNECTORS.has(lower) ||
|
|
1651
|
+
BODY_SITE_HINTS.has(lower) ||
|
|
1652
|
+
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1420
1653
|
indicesToInclude.add(token.index);
|
|
1421
1654
|
prev -= 1;
|
|
1422
1655
|
continue;
|
|
@@ -1430,7 +1663,9 @@ function parseInternal(input, options) {
|
|
|
1430
1663
|
break;
|
|
1431
1664
|
}
|
|
1432
1665
|
const lower = token.lower;
|
|
1433
|
-
if (SITE_CONNECTORS.has(lower) ||
|
|
1666
|
+
if (SITE_CONNECTORS.has(lower) ||
|
|
1667
|
+
BODY_SITE_HINTS.has(lower) ||
|
|
1668
|
+
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1434
1669
|
indicesToInclude.add(token.index);
|
|
1435
1670
|
next += 1;
|
|
1436
1671
|
continue;
|
|
@@ -1456,9 +1691,17 @@ function parseInternal(input, options) {
|
|
|
1456
1691
|
.join(" ")
|
|
1457
1692
|
.trim();
|
|
1458
1693
|
if (normalizedSite) {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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";
|
|
1704
|
+
}
|
|
1462
1705
|
}
|
|
1463
1706
|
}
|
|
1464
1707
|
}
|
package/dist/schedule.js
CHANGED
|
@@ -495,6 +495,12 @@ function nextDueDoses(dosage, options) {
|
|
|
495
495
|
if (!timing || !repeat) {
|
|
496
496
|
return [];
|
|
497
497
|
}
|
|
498
|
+
const rawCount = repeat.count;
|
|
499
|
+
const normalizedCount = rawCount === undefined ? undefined : Math.max(0, Math.floor(rawCount));
|
|
500
|
+
if (normalizedCount === 0) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
const effectiveLimit = normalizedCount !== undefined ? Math.min(limit, normalizedCount) : limit;
|
|
498
504
|
const results = [];
|
|
499
505
|
const seen = new Set();
|
|
500
506
|
const dayFilter = new Set(((_g = repeat.dayOfWeek) !== null && _g !== void 0 ? _g : []).map((day) => day.toLowerCase()));
|
|
@@ -519,17 +525,22 @@ function nextDueDoses(dosage, options) {
|
|
|
519
525
|
const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
|
|
520
526
|
if (!orderedAt || orderedAt >= from) {
|
|
521
527
|
const instantIso = formatZonedIso(immediateSource, timeZone);
|
|
522
|
-
|
|
523
|
-
|
|
528
|
+
if (!seen.has(instantIso)) {
|
|
529
|
+
seen.add(instantIso);
|
|
530
|
+
results.push(instantIso);
|
|
531
|
+
}
|
|
524
532
|
}
|
|
525
533
|
}
|
|
534
|
+
if (results.length >= effectiveLimit) {
|
|
535
|
+
return results.slice(0, effectiveLimit);
|
|
536
|
+
}
|
|
526
537
|
if (expanded.length === 0) {
|
|
527
|
-
return results.slice(0,
|
|
538
|
+
return results.slice(0, effectiveLimit);
|
|
528
539
|
}
|
|
529
540
|
let currentDay = startOfLocalDay(from, timeZone);
|
|
530
541
|
let iterations = 0;
|
|
531
|
-
const maxIterations =
|
|
532
|
-
while (results.length <
|
|
542
|
+
const maxIterations = effectiveLimit * 31;
|
|
543
|
+
while (results.length < effectiveLimit && iterations < maxIterations) {
|
|
533
544
|
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
534
545
|
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
535
546
|
for (const entry of expanded) {
|
|
@@ -550,16 +561,19 @@ function nextDueDoses(dosage, options) {
|
|
|
550
561
|
if (!seen.has(iso)) {
|
|
551
562
|
seen.add(iso);
|
|
552
563
|
results.push(iso);
|
|
553
|
-
if (results.length ===
|
|
564
|
+
if (results.length === effectiveLimit) {
|
|
554
565
|
break;
|
|
555
566
|
}
|
|
556
567
|
}
|
|
557
568
|
}
|
|
558
569
|
}
|
|
570
|
+
if (results.length >= effectiveLimit) {
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
559
573
|
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
560
574
|
iterations += 1;
|
|
561
575
|
}
|
|
562
|
-
return results.slice(0,
|
|
576
|
+
return results.slice(0, effectiveLimit);
|
|
563
577
|
}
|
|
564
578
|
const treatAsInterval = !!repeat.period &&
|
|
565
579
|
!!repeat.periodUnit &&
|
|
@@ -569,7 +583,7 @@ function nextDueDoses(dosage, options) {
|
|
|
569
583
|
if (treatAsInterval) {
|
|
570
584
|
// True interval schedules advance from the order start in fixed units. The
|
|
571
585
|
// timing.code remains advisory so we only rely on the period/unit fields.
|
|
572
|
-
const candidates = generateIntervalSeries(baseTime, from,
|
|
586
|
+
const candidates = generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt);
|
|
573
587
|
return candidates;
|
|
574
588
|
}
|
|
575
589
|
if (repeat.frequency && repeat.period && repeat.periodUnit) {
|
|
@@ -582,8 +596,8 @@ function nextDueDoses(dosage, options) {
|
|
|
582
596
|
}
|
|
583
597
|
let currentDay = startOfLocalDay(from, timeZone);
|
|
584
598
|
let iterations = 0;
|
|
585
|
-
const maxIterations =
|
|
586
|
-
while (results.length <
|
|
599
|
+
const maxIterations = effectiveLimit * 31;
|
|
600
|
+
while (results.length < effectiveLimit && iterations < maxIterations) {
|
|
587
601
|
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
588
602
|
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
589
603
|
for (const clock of clocks) {
|
|
@@ -601,7 +615,7 @@ function nextDueDoses(dosage, options) {
|
|
|
601
615
|
if (!seen.has(iso)) {
|
|
602
616
|
seen.add(iso);
|
|
603
617
|
results.push(iso);
|
|
604
|
-
if (results.length ===
|
|
618
|
+
if (results.length === effectiveLimit) {
|
|
605
619
|
break;
|
|
606
620
|
}
|
|
607
621
|
}
|
|
@@ -610,7 +624,7 @@ function nextDueDoses(dosage, options) {
|
|
|
610
624
|
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
611
625
|
iterations += 1;
|
|
612
626
|
}
|
|
613
|
-
return results.slice(0,
|
|
627
|
+
return results.slice(0, effectiveLimit);
|
|
614
628
|
}
|
|
615
629
|
return [];
|
|
616
630
|
}
|
|
@@ -618,7 +632,7 @@ function nextDueDoses(dosage, options) {
|
|
|
618
632
|
* Generates an interval-based series by stepping forward from the base time
|
|
619
633
|
* until the requested number of timestamps have been produced.
|
|
620
634
|
*/
|
|
621
|
-
function generateIntervalSeries(baseTime, from,
|
|
635
|
+
function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt) {
|
|
622
636
|
const increment = createIntervalStepper(repeat, timeZone);
|
|
623
637
|
if (!increment) {
|
|
624
638
|
return [];
|
|
@@ -627,7 +641,7 @@ function generateIntervalSeries(baseTime, from, limit, repeat, timeZone, dayFilt
|
|
|
627
641
|
const seen = new Set();
|
|
628
642
|
let current = baseTime;
|
|
629
643
|
let guard = 0;
|
|
630
|
-
const maxIterations =
|
|
644
|
+
const maxIterations = effectiveLimit * 1000;
|
|
631
645
|
while (current < from && guard < maxIterations) {
|
|
632
646
|
const next = increment(current);
|
|
633
647
|
if (!next || next.getTime() === current.getTime()) {
|
|
@@ -636,7 +650,7 @@ function generateIntervalSeries(baseTime, from, limit, repeat, timeZone, dayFilt
|
|
|
636
650
|
current = next;
|
|
637
651
|
guard += 1;
|
|
638
652
|
}
|
|
639
|
-
while (results.length <
|
|
653
|
+
while (results.length < effectiveLimit && guard < maxIterations) {
|
|
640
654
|
const weekday = getLocalWeekday(current, timeZone);
|
|
641
655
|
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
642
656
|
if (current < from) {
|
|
@@ -671,7 +685,7 @@ function generateIntervalSeries(baseTime, from, limit, repeat, timeZone, dayFilt
|
|
|
671
685
|
current = next;
|
|
672
686
|
guard += 1;
|
|
673
687
|
}
|
|
674
|
-
return results.slice(0,
|
|
688
|
+
return results.slice(0, effectiveLimit);
|
|
675
689
|
}
|
|
676
690
|
/**
|
|
677
691
|
* Builds a function that advances a Date according to repeat.period/unit.
|
package/dist/types.d.ts
CHANGED