ezmedicationinput 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- frequency: (_l = (_k = dosage.timing) === null || _k === void 0 ? void 0 : _k.repeat) === null || _l === void 0 ? void 0 : _l.frequency,
129
- frequencyMax: (_o = (_m = dosage.timing) === null || _m === void 0 ? void 0 : _m.repeat) === null || _o === void 0 ? void 0 : _o.frequencyMax,
130
- period: (_q = (_p = dosage.timing) === null || _p === void 0 ? void 0 : _p.repeat) === null || _q === void 0 ? void 0 : _q.period,
131
- periodMax: (_s = (_r = dosage.timing) === null || _r === void 0 ? void 0 : _r.repeat) === null || _s === void 0 ? void 0 : _s.periodMax,
132
- periodUnit: (_u = (_t = dosage.timing) === null || _t === void 0 ? void 0 : _t.repeat) === null || _u === void 0 ? void 0 : _u.periodUnit,
133
- routeText: (_v = dosage.route) === null || _v === void 0 ? void 0 : _v.text,
134
- siteText: (_w = dosage.site) === null || _w === void 0 ? void 0 : _w.text,
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: (_y = (_x = dosage.asNeededFor) === null || _x === void 0 ? void 0 : _x[0]) === null || _y === void 0 ? void 0 : _y.text,
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 = (_0 = (_z = dosage.route) === null || _z === void 0 ? void 0 : _z.coding) === null || _0 === void 0 ? void 0 : _0.find((code) => code.system === SNOMED_SYSTEM);
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 = (_1 = dosage.doseAndRate) === null || _1 === void 0 ? void 0 : _1[0];
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 = (_3 = (_2 = low === null || low === void 0 ? void 0 : low.unit) !== null && _2 !== void 0 ? _2 : high === null || high === void 0 ? void 0 : high.unit) !== null && _3 !== void 0 ? _3 : 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
  }
@@ -16,6 +16,7 @@ export interface ParsedSigInternal {
16
16
  unit?: string;
17
17
  routeCode?: RouteCode;
18
18
  routeText?: string;
19
+ count?: number;
19
20
  frequency?: number;
20
21
  frequencyMax?: number;
21
22
  period?: number;
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) || BODY_SITE_HINTS.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) || BODY_SITE_HINTS.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
- internal.siteText = normalizedSite;
1460
- if (!internal.siteSource) {
1461
- internal.siteSource = "text";
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
- results.push(instantIso);
523
- seen.add(instantIso);
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, limit);
538
+ return results.slice(0, effectiveLimit);
528
539
  }
529
540
  let currentDay = startOfLocalDay(from, timeZone);
530
541
  let iterations = 0;
531
- const maxIterations = limit * 31;
532
- while (results.length < limit && iterations < maxIterations) {
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 === limit) {
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, limit);
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, limit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt);
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 = limit * 31;
586
- while (results.length < limit && iterations < maxIterations) {
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 === limit) {
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, limit);
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, limit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt) {
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 = limit * 1000;
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 < limit && guard < maxIterations) {
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, limit);
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/suggest.js CHANGED
@@ -105,6 +105,7 @@ const WHEN_COMBINATIONS = [
105
105
  ].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
106
106
  const CORE_WHEN_TOKENS = ["pc", "ac", "hs"].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
107
107
  const FREQUENCY_CODES = ["qd", "od", "bid", "tid", "qid"].filter((token) => maps_1.TIMING_ABBREVIATIONS[token] !== undefined);
108
+ const FREQUENCY_CODE_SUFFIXES = FREQUENCY_CODES.map((code) => ` ${code}`);
108
109
  const FREQ_TOKEN_BY_NUMBER = {};
109
110
  for (const [frequency, token] of [
110
111
  [1, "qd"],
@@ -235,10 +236,15 @@ function removeDashes(value) {
235
236
  }
236
237
  return result.join("");
237
238
  }
239
+ const UNIT_VARIANT_CACHE = new Map();
238
240
  function getUnitVariants(unit) {
239
241
  var _a;
240
242
  const canonical = (_a = resolveCanonicalUnit(unit)) !== null && _a !== void 0 ? _a : normalizeSpacing(unit);
241
243
  const normalizedCanonical = normalizeKey(canonical);
244
+ const cached = UNIT_VARIANT_CACHE.get(normalizedCanonical);
245
+ if (cached) {
246
+ return cached;
247
+ }
242
248
  const variants = new Map();
243
249
  const push = (candidate) => {
244
250
  if (!candidate) {
@@ -262,7 +268,9 @@ function getUnitVariants(unit) {
262
268
  push(candidate);
263
269
  }
264
270
  }
265
- return [...variants.values()];
271
+ const result = [...variants.values()];
272
+ UNIT_VARIANT_CACHE.set(normalizedCanonical, result);
273
+ return result;
266
274
  }
267
275
  function buildIntervalTokens(input) {
268
276
  const intervals = new Set();
@@ -322,6 +330,7 @@ function buildWhenSequences() {
322
330
  return sequences;
323
331
  }
324
332
  const PRECOMPUTED_WHEN_SEQUENCES = buildWhenSequences();
333
+ const PRECOMPUTED_WHEN_SEQUENCE_SUFFIXES = PRECOMPUTED_WHEN_SEQUENCES.map((sequence) => ` ${sequence.join(" ")}`);
325
334
  function tokenizeLowercaseForMatching(value) {
326
335
  return value
327
336
  .split(/\s+/)
@@ -481,117 +490,121 @@ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalToke
481
490
  suggestions.push(value);
482
491
  return suggestions.length >= limit;
483
492
  };
484
- for (const pair of pairs) {
493
+ const codeSuffixes = FREQUENCY_CODE_SUFFIXES;
494
+ const prnSuffixes = new Array(prnReasons.length);
495
+ for (let i = 0; i < prnReasons.length; i += 1) {
496
+ prnSuffixes[i] = ` prn ${prnReasons[i]}`;
497
+ }
498
+ const intervalSuffixes = new Array(intervalTokens.length);
499
+ for (let i = 0; i < intervalTokens.length; i += 1) {
500
+ intervalSuffixes[i] = ` ${intervalTokens[i]}`;
501
+ }
502
+ const whenSuffixes = whenSequences === PRECOMPUTED_WHEN_SEQUENCES
503
+ ? PRECOMPUTED_WHEN_SEQUENCE_SUFFIXES
504
+ : whenSequences.map((sequence) => ` ${sequence.join(" ")}`);
505
+ for (let pairIndex = 0; pairIndex < pairs.length; pairIndex += 1) {
506
+ const pair = pairs[pairIndex];
485
507
  const unitVariants = getUnitVariants(pair.unit);
486
508
  const route = pair.route;
487
509
  const routeLower = pair.routeLower;
488
- for (const code of FREQUENCY_CODES) {
489
- const codeSuffix = ` ${code}`;
490
- for (const unitVariant of unitVariants) {
491
- const unitRoute = `${unitVariant.value} ${route}`;
492
- const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
493
- for (const doseVariant of doseVariants) {
494
- const candidate = `${doseVariant.value} ${unitRoute}${codeSuffix}`;
495
- const candidateLower = `${doseVariant.lower} ${unitRouteLower}${codeSuffix}`;
496
- if (push(candidate, candidateLower)) {
510
+ const unitDoseVariants = new Array(unitVariants.length);
511
+ for (let unitIndex = 0; unitIndex < unitVariants.length; unitIndex += 1) {
512
+ const unitVariant = unitVariants[unitIndex];
513
+ const unitRouteValue = `${unitVariant.value} ${route}`;
514
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
515
+ const doseBases = new Array(doseVariants.length);
516
+ for (let doseIndex = 0; doseIndex < doseVariants.length; doseIndex += 1) {
517
+ const doseVariant = doseVariants[doseIndex];
518
+ doseBases[doseIndex] = {
519
+ value: `${doseVariant.value} ${unitRouteValue}`,
520
+ lower: `${doseVariant.lower} ${unitRouteLower}`,
521
+ };
522
+ }
523
+ unitDoseVariants[unitIndex] = doseBases;
524
+ }
525
+ for (let codeIndex = 0; codeIndex < codeSuffixes.length; codeIndex += 1) {
526
+ const codeSuffix = codeSuffixes[codeIndex];
527
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
528
+ const doseBases = unitDoseVariants[unitIndex];
529
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
530
+ const base = doseBases[doseIndex];
531
+ if (push(base.value + codeSuffix, base.lower + codeSuffix)) {
497
532
  return suggestions;
498
533
  }
499
534
  }
500
535
  }
501
- const candidate = `${route}${codeSuffix}`;
502
- const candidateLower = `${routeLower}${codeSuffix}`;
503
- if (push(candidate, candidateLower)) {
536
+ if (push(route + codeSuffix, routeLower + codeSuffix)) {
504
537
  return suggestions;
505
538
  }
506
539
  }
507
- for (const interval of intervalTokens) {
508
- const intervalSuffix = ` ${interval}`;
509
- for (const unitVariant of unitVariants) {
510
- const unitRoute = `${unitVariant.value} ${route}`;
511
- const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
512
- for (const doseVariant of doseVariants) {
513
- const base = `${doseVariant.value} ${unitRoute}`;
514
- const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
515
- const intervalCandidate = `${base}${intervalSuffix}`;
516
- const intervalCandidateLower = `${baseLower}${intervalSuffix}`;
517
- if (push(intervalCandidate, intervalCandidateLower)) {
540
+ for (let intervalIndex = 0; intervalIndex < intervalSuffixes.length; intervalIndex += 1) {
541
+ const intervalSuffix = intervalSuffixes[intervalIndex];
542
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
543
+ const doseBases = unitDoseVariants[unitIndex];
544
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
545
+ const base = doseBases[doseIndex];
546
+ const baseIntervalValue = base.value + intervalSuffix;
547
+ const baseIntervalLower = base.lower + intervalSuffix;
548
+ if (push(baseIntervalValue, baseIntervalLower)) {
518
549
  return suggestions;
519
550
  }
520
- for (const reason of prnReasons) {
521
- const reasonSuffix = `${intervalSuffix} prn ${reason}`;
522
- const reasonCandidate = `${base}${reasonSuffix}`;
523
- const reasonCandidateLower = `${baseLower}${reasonSuffix}`;
524
- if (push(reasonCandidate, reasonCandidateLower)) {
551
+ for (let reasonIndex = 0; reasonIndex < prnSuffixes.length; reasonIndex += 1) {
552
+ const reasonSuffix = prnSuffixes[reasonIndex];
553
+ if (push(baseIntervalValue + reasonSuffix, baseIntervalLower + reasonSuffix)) {
525
554
  return suggestions;
526
555
  }
527
556
  }
528
557
  }
529
558
  }
530
- const candidate = `${route}${intervalSuffix}`;
531
- const candidateLower = `${routeLower}${intervalSuffix}`;
532
- if (push(candidate, candidateLower)) {
559
+ if (push(route + intervalSuffix, routeLower + intervalSuffix)) {
533
560
  return suggestions;
534
561
  }
535
562
  }
536
- for (const freq of FREQUENCY_NUMBERS) {
563
+ for (let freqIndex = 0; freqIndex < FREQUENCY_NUMBERS.length; freqIndex += 1) {
564
+ const freq = FREQUENCY_NUMBERS[freqIndex];
537
565
  const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
538
566
  if (!freqToken) {
539
567
  continue;
540
568
  }
541
- const base = `1x${freq} ${route}`;
569
+ const baseValue = `1x${freq} ${route}`;
542
570
  const baseLower = `1x${freq} ${routeLower}`;
543
- const freqCandidate = `${base} ${freqToken}`;
544
- const freqCandidateLower = `${baseLower} ${freqToken}`;
545
- if (push(freqCandidate, freqCandidateLower)) {
571
+ if (push(`${baseValue} ${freqToken}`, `${baseLower} ${freqToken}`)) {
546
572
  return suggestions;
547
573
  }
548
- for (const when of CORE_WHEN_TOKENS) {
549
- const whenCandidate = `${base} ${when}`;
550
- const whenCandidateLower = `${baseLower} ${when}`;
551
- if (push(whenCandidate, whenCandidateLower)) {
574
+ for (let whenIndex = 0; whenIndex < CORE_WHEN_TOKENS.length; whenIndex += 1) {
575
+ const whenToken = CORE_WHEN_TOKENS[whenIndex];
576
+ if (push(`${baseValue} ${whenToken}`, `${baseLower} ${whenToken}`)) {
552
577
  return suggestions;
553
578
  }
554
579
  }
555
580
  }
556
- for (const whenSequence of whenSequences) {
557
- const suffix = ` ${whenSequence.join(" ")}`;
558
- for (const unitVariant of unitVariants) {
559
- const unitRoute = `${unitVariant.value} ${route}`;
560
- const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
561
- for (const doseVariant of doseVariants) {
562
- const base = `${doseVariant.value} ${unitRoute}`;
563
- const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
564
- const candidate = `${base}${suffix}`;
565
- const candidateLower = `${baseLower}${suffix}`;
566
- if (push(candidate, candidateLower)) {
581
+ for (let whenIndex = 0; whenIndex < whenSuffixes.length; whenIndex += 1) {
582
+ const whenSuffix = whenSuffixes[whenIndex];
583
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
584
+ const doseBases = unitDoseVariants[unitIndex];
585
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
586
+ const base = doseBases[doseIndex];
587
+ if (push(base.value + whenSuffix, base.lower + whenSuffix)) {
567
588
  return suggestions;
568
589
  }
569
590
  }
570
591
  }
571
- const candidate = `${route}${suffix}`;
572
- const candidateLower = `${routeLower}${suffix}`;
573
- if (push(candidate, candidateLower)) {
592
+ if (push(route + whenSuffix, routeLower + whenSuffix)) {
574
593
  return suggestions;
575
594
  }
576
595
  }
577
- for (const reason of prnReasons) {
578
- const reasonSuffix = ` prn ${reason}`;
579
- for (const unitVariant of unitVariants) {
580
- const unitRoute = `${unitVariant.value} ${route}`;
581
- const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
582
- for (const doseVariant of doseVariants) {
583
- const base = `${doseVariant.value} ${unitRoute}`;
584
- const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
585
- const candidate = `${base}${reasonSuffix}`;
586
- const candidateLower = `${baseLower}${reasonSuffix}`;
587
- if (push(candidate, candidateLower)) {
596
+ for (let reasonIndex = 0; reasonIndex < prnSuffixes.length; reasonIndex += 1) {
597
+ const reasonSuffix = prnSuffixes[reasonIndex];
598
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
599
+ const doseBases = unitDoseVariants[unitIndex];
600
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
601
+ const base = doseBases[doseIndex];
602
+ if (push(base.value + reasonSuffix, base.lower + reasonSuffix)) {
588
603
  return suggestions;
589
604
  }
590
605
  }
591
606
  }
592
- const candidate = `${route}${reasonSuffix}`;
593
- const candidateLower = `${routeLower}${reasonSuffix}`;
594
- if (push(candidate, candidateLower)) {
607
+ if (push(route + reasonSuffix, routeLower + reasonSuffix)) {
595
608
  return suggestions;
596
609
  }
597
610
  }
package/dist/types.d.ts CHANGED
@@ -241,6 +241,7 @@ export declare enum FhirDayOfWeek {
241
241
  Sunday = "sun"
242
242
  }
243
243
  export interface FhirTimingRepeat {
244
+ count?: number;
244
245
  frequency?: number;
245
246
  frequencyMax?: number;
246
247
  period?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",