ezmedicationinput 0.1.2 → 0.1.3

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
@@ -49,6 +49,39 @@ Example output:
49
49
  }
50
50
  ```
51
51
 
52
+ ### Sig (directions) suggestions
53
+
54
+ Use `suggestSig` to drive autocomplete experiences while the clinician is
55
+ typing shorthand medication directions (sig = directions). It returns an array
56
+ of canonical direction strings and accepts the same `ParseOptions` context plus
57
+ a `limit` and custom PRN reasons.
58
+
59
+ ```ts
60
+ import { suggestSig } from "ezmedicationinput";
61
+
62
+ const suggestions = suggestSig("1 drop to od q2h", {
63
+ limit: 5,
64
+ context: { dosageForm: "ophthalmic solution" },
65
+ });
66
+
67
+ // → ["1 drop oph q2h", "1 drop oph q2h prn pain", ...]
68
+ ```
69
+
70
+ Highlights:
71
+
72
+ - Recognizes plural units and their singular counterparts (`tab`/`tabs`,
73
+ `puff`/`puffs`, `mL`/`millilitres`, etc.) and normalizes spelled-out metric,
74
+ SI-prefixed masses/volumes (`micrograms`, `microliters`, `nanograms`,
75
+ `liters`, `kilograms`, etc.) alongside household measures like `teaspoon`
76
+ and `tablespoons` (set `allowHouseholdVolumeUnits: false` to omit them).
77
+ - Keeps matching even when intermediary words such as `to`, `in`, or ocular
78
+ site shorthand (`od`, `os`, `ou`) appear in the prefix.
79
+ - Emits dynamic interval suggestions, including arbitrary `q<number>h` cadences
80
+ and common range patterns like `q4-6h`.
81
+ - Supports multiple timing tokens in sequence (e.g. `1 tab po morn hs`).
82
+ - Surfaces PRN reasons from built-ins or custom `prnReasons` entries while
83
+ preserving numeric doses pulled from the typed prefix.
84
+
52
85
  ## Dictionaries
53
86
 
54
87
  The library exposes default dictionaries in `maps.ts` for routes, units, frequencies (Timing abbreviations + repeat defaults), and event timing tokens. You can extend or override them via the `ParseOptions` argument.
@@ -63,8 +96,12 @@ Key EventTiming mappings include:
63
96
  | `pc breakfast` | `PCM`
64
97
  | `pc lunch` | `PCD`
65
98
  | `pc dinner` | `PCV`
99
+ | `breakfast`, `bfast`, `brkfst`, `brk` | `CM`
100
+ | `lunch`, `lunchtime` | `CD`
101
+ | `dinner`, `dinnertime`, `supper`, `suppertime` | `CV`
66
102
  | `am`, `morning` | `MORN`
67
- | `noon` | `NOON`
103
+ | `noon`, `midday`, `mid-day` | `NOON`
104
+ | `afternoon`, `aft` | `AFT`
68
105
  | `pm`, `evening` | `EVE`
69
106
  | `night` | `NIGHT`
70
107
  | `hs`, `bedtime` | `HS`
@@ -82,6 +119,9 @@ Routes always include SNOMED CT codings. Every code from the SNOMED Route of Adm
82
119
  `null` to explicitly disable context-based inference.
83
120
  - `smartMealExpansion`: when `true`, generic AC/PC/C tokens expand into specific EventTiming combinations (e.g. `1x2 po ac` → `ACM` + `ACV`).
84
121
  - `twoPerDayPair`: controls whether 2× AC/PC/C doses expand to breakfast+dinner (default) or breakfast+lunch.
122
+ - `eventClock`: optional map of `EventTiming` codes to HH:mm strings that drives chronological ordering of parsed `when` values.
123
+ - `allowHouseholdVolumeUnits`: defaults to `true`; set to `false` to ignore
124
+ teaspoon/tablespoon units during parsing and suggestions.
85
125
  - Custom `routeMap`, `unitMap`, `freqMap`, and `whenMap` let you augment the built-in dictionaries without mutating them.
86
126
 
87
127
  ### Next due dose generation
package/dist/maps.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface RouteSynonym {
19
19
  text: string;
20
20
  }
21
21
  export declare const DEFAULT_ROUTE_SYNONYMS: Record<string, RouteSynonym>;
22
+ export declare const HOUSEHOLD_VOLUME_UNITS: readonly ["tsp", "tbsp"];
22
23
  export declare const DEFAULT_UNIT_SYNONYMS: Record<string, string>;
23
24
  export interface FrequencyDescriptor {
24
25
  code?: string;
package/dist/maps.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_UNIT_BY_ROUTE = exports.DEFAULT_UNIT_BY_NORMALIZED_FORM = exports.KNOWN_TMT_DOSAGE_FORM_TO_SNOMED_ROUTE = exports.KNOWN_DOSAGE_FORMS_TO_DOSE = exports.WORD_FREQUENCIES = exports.DAY_OF_WEEK_TOKENS = exports.DISCOURAGED_TOKENS = exports.MEAL_KEYWORDS = exports.EVENT_TIMING_TOKENS = exports.TIMING_ABBREVIATIONS = exports.DEFAULT_UNIT_SYNONYMS = exports.DEFAULT_ROUTE_SYNONYMS = exports.ROUTE_BY_SNOMED = exports.ROUTE_TEXT = exports.ROUTE_SNOMED = void 0;
3
+ exports.DEFAULT_UNIT_BY_ROUTE = exports.DEFAULT_UNIT_BY_NORMALIZED_FORM = exports.KNOWN_TMT_DOSAGE_FORM_TO_SNOMED_ROUTE = exports.KNOWN_DOSAGE_FORMS_TO_DOSE = exports.WORD_FREQUENCIES = exports.DAY_OF_WEEK_TOKENS = exports.DISCOURAGED_TOKENS = exports.MEAL_KEYWORDS = exports.EVENT_TIMING_TOKENS = exports.TIMING_ABBREVIATIONS = exports.DEFAULT_UNIT_SYNONYMS = exports.HOUSEHOLD_VOLUME_UNITS = exports.DEFAULT_ROUTE_SYNONYMS = exports.ROUTE_BY_SNOMED = exports.ROUTE_TEXT = exports.ROUTE_SNOMED = void 0;
4
4
  const types_1 = require("./types");
5
5
  const object_1 = require("./utils/object");
6
6
  const ROUTE_TEXT_OVERRIDES = {
@@ -140,7 +140,92 @@ exports.DEFAULT_ROUTE_SYNONYMS = (() => {
140
140
  }
141
141
  return map;
142
142
  })();
143
- exports.DEFAULT_UNIT_SYNONYMS = {
143
+ const UNIT_PREFIXES = [
144
+ { canonical: "", abbreviations: [""], names: [{ singular: "", plural: "" }] },
145
+ {
146
+ canonical: "m",
147
+ abbreviations: ["m"],
148
+ names: [{ singular: "milli", plural: "milli" }],
149
+ },
150
+ {
151
+ canonical: "mc",
152
+ abbreviations: ["mc", "µ", "μ", "u"],
153
+ names: [{ singular: "micro", plural: "micro" }],
154
+ },
155
+ {
156
+ canonical: "n",
157
+ abbreviations: ["n"],
158
+ names: [{ singular: "nano", plural: "nano" }],
159
+ },
160
+ {
161
+ canonical: "k",
162
+ abbreviations: ["k"],
163
+ names: [{ singular: "kilo", plural: "kilo" }],
164
+ },
165
+ ];
166
+ const METRIC_UNIT_BASES = [
167
+ {
168
+ canonical: "g",
169
+ abbreviations: ["g"],
170
+ names: [
171
+ { singular: "gram", plural: "grams" },
172
+ { singular: "gramme", plural: "grammes" },
173
+ ],
174
+ },
175
+ {
176
+ canonical: "L",
177
+ abbreviations: ["l"],
178
+ names: [
179
+ { singular: "liter", plural: "liters" },
180
+ { singular: "litre", plural: "litres" },
181
+ ],
182
+ },
183
+ ];
184
+ function assignUnitSynonym(map, key, canonical) {
185
+ const normalized = key.trim().toLowerCase();
186
+ if (!normalized || map[normalized]) {
187
+ return;
188
+ }
189
+ map[normalized] = canonical;
190
+ }
191
+ function addMetricUnitSynonyms(map) {
192
+ for (const prefix of UNIT_PREFIXES) {
193
+ for (const base of METRIC_UNIT_BASES) {
194
+ const canonical = `${prefix.canonical}${base.canonical}`;
195
+ for (const prefixAbbrev of prefix.abbreviations) {
196
+ for (const baseAbbrev of base.abbreviations) {
197
+ if (!baseAbbrev) {
198
+ continue;
199
+ }
200
+ const token = `${prefixAbbrev}${baseAbbrev}`;
201
+ assignUnitSynonym(map, token, canonical);
202
+ assignUnitSynonym(map, `${token}s`, canonical);
203
+ if (token.endsWith(".")) {
204
+ assignUnitSynonym(map, token.replace(/\.+$/, ""), canonical);
205
+ }
206
+ }
207
+ }
208
+ for (const prefixName of prefix.names) {
209
+ for (const baseName of base.names) {
210
+ const singular = `${prefixName.singular}${baseName.singular}`;
211
+ const plural = `${prefixName.singular}${baseName.plural}`;
212
+ const hyphenSingular = prefixName.singular
213
+ ? `${prefixName.singular}-${baseName.singular}`
214
+ : baseName.singular;
215
+ const hyphenPlural = prefixName.singular
216
+ ? `${prefixName.singular}-${baseName.plural}`
217
+ : baseName.plural;
218
+ assignUnitSynonym(map, singular, canonical);
219
+ assignUnitSynonym(map, plural, canonical);
220
+ assignUnitSynonym(map, hyphenSingular, canonical);
221
+ assignUnitSynonym(map, hyphenPlural, canonical);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ exports.HOUSEHOLD_VOLUME_UNITS = ["tsp", "tbsp"];
228
+ const STATIC_UNIT_SYNONYMS = {
144
229
  tab: "tab",
145
230
  tabs: "tab",
146
231
  tablet: "tab",
@@ -149,13 +234,6 @@ exports.DEFAULT_UNIT_SYNONYMS = {
149
234
  caps: "cap",
150
235
  capsule: "cap",
151
236
  capsules: "cap",
152
- ml: "mL",
153
- "milliliter": "mL",
154
- "milliliters": "mL",
155
- mg: "mg",
156
- g: "g",
157
- mcg: "mcg",
158
- ug: "mcg",
159
237
  puff: "puff",
160
238
  puffs: "puff",
161
239
  spray: "spray",
@@ -166,8 +244,25 @@ exports.DEFAULT_UNIT_SYNONYMS = {
166
244
  patches: "patch",
167
245
  supp: "suppository",
168
246
  suppository: "suppository",
169
- suppositories: "suppository"
247
+ suppositories: "suppository",
248
+ tsp: "tsp",
249
+ "tsp.": "tsp",
250
+ tsps: "tsp",
251
+ "tsps.": "tsp",
252
+ teaspoon: "tsp",
253
+ teaspoons: "tsp",
254
+ tbsp: "tbsp",
255
+ "tbsp.": "tbsp",
256
+ tbs: "tbsp",
257
+ "tbs.": "tbsp",
258
+ tablespoon: "tbsp",
259
+ tablespoons: "tbsp",
170
260
  };
261
+ exports.DEFAULT_UNIT_SYNONYMS = (() => {
262
+ const map = Object.assign({}, STATIC_UNIT_SYNONYMS);
263
+ addMetricUnitSynonyms(map);
264
+ return map;
265
+ })();
171
266
  exports.TIMING_ABBREVIATIONS = {
172
267
  qd: {
173
268
  code: "QD",
@@ -256,12 +351,26 @@ exports.EVENT_TIMING_TOKENS = {
256
351
  "@meal": types_1.EventTiming.Meal,
257
352
  "@meals": types_1.EventTiming.Meal,
258
353
  cm: types_1.EventTiming.Breakfast,
354
+ breakfast: types_1.EventTiming.Breakfast,
355
+ bfast: types_1.EventTiming.Breakfast,
356
+ brkfst: types_1.EventTiming.Breakfast,
357
+ brk: types_1.EventTiming.Breakfast,
259
358
  cd: types_1.EventTiming.Lunch,
359
+ lunch: types_1.EventTiming.Lunch,
360
+ lunchtime: types_1.EventTiming.Lunch,
260
361
  cv: types_1.EventTiming.Dinner,
362
+ dinner: types_1.EventTiming.Dinner,
363
+ dinnertime: types_1.EventTiming.Dinner,
364
+ supper: types_1.EventTiming.Dinner,
365
+ suppertime: types_1.EventTiming.Dinner,
261
366
  am: types_1.EventTiming.Morning,
262
367
  morning: types_1.EventTiming.Morning,
263
368
  morn: types_1.EventTiming.Morning,
264
369
  noon: types_1.EventTiming.Noon,
370
+ midday: types_1.EventTiming.Noon,
371
+ "mid-day": types_1.EventTiming.Noon,
372
+ afternoon: types_1.EventTiming.Afternoon,
373
+ aft: types_1.EventTiming.Afternoon,
265
374
  pm: types_1.EventTiming.Evening,
266
375
  evening: types_1.EventTiming.Evening,
267
376
  night: types_1.EventTiming.Night,
@@ -271,12 +380,25 @@ exports.EVENT_TIMING_TOKENS = {
271
380
  waking: types_1.EventTiming.Wake,
272
381
  stat: types_1.EventTiming.Immediate
273
382
  };
274
- exports.MEAL_KEYWORDS = {
275
- breakfast: { pc: types_1.EventTiming["After Breakfast"], ac: types_1.EventTiming["Before Breakfast"] },
276
- lunch: { pc: types_1.EventTiming["After Lunch"], ac: types_1.EventTiming["Before Lunch"] },
277
- dinner: { pc: types_1.EventTiming["After Dinner"], ac: types_1.EventTiming["Before Dinner"] },
278
- supper: { pc: types_1.EventTiming["After Dinner"], ac: types_1.EventTiming["Before Dinner"] }
279
- };
383
+ const MEAL_KEYWORD_ENTRIES = [];
384
+ function registerMealKeywords(keys, meal) {
385
+ for (const key of keys) {
386
+ MEAL_KEYWORD_ENTRIES.push([key, meal]);
387
+ }
388
+ }
389
+ registerMealKeywords(["breakfast", "bfast", "brkfst", "brk"], {
390
+ pc: types_1.EventTiming["After Breakfast"],
391
+ ac: types_1.EventTiming["Before Breakfast"]
392
+ });
393
+ registerMealKeywords(["lunch", "lunchtime"], {
394
+ pc: types_1.EventTiming["After Lunch"],
395
+ ac: types_1.EventTiming["Before Lunch"]
396
+ });
397
+ registerMealKeywords(["dinner", "dinnertime", "supper", "suppertime"], {
398
+ pc: types_1.EventTiming["After Dinner"],
399
+ ac: types_1.EventTiming["Before Dinner"]
400
+ });
401
+ exports.MEAL_KEYWORDS = (0, object_1.objectFromEntries)(MEAL_KEYWORD_ENTRIES);
280
402
  exports.DISCOURAGED_TOKENS = {
281
403
  qd: "QD",
282
404
  qod: "QOD",
@@ -555,6 +677,8 @@ exports.KNOWN_TMT_DOSAGE_FORM_TO_SNOMED_ROUTE = {
555
677
  collodion: types_1.SNOMEDCTRouteCodes["Oral route"],
556
678
  "powder for rectal solution": types_1.SNOMEDCTRouteCodes["Per rectum"],
557
679
  "eye drops, solution": types_1.SNOMEDCTRouteCodes["Ocular route (qualifier value)"],
680
+ "eye drops": types_1.SNOMEDCTRouteCodes["Ocular route (qualifier value)"],
681
+ "eye drop": types_1.SNOMEDCTRouteCodes["Ocular route (qualifier value)"],
558
682
  "oromucosal paste": types_1.SNOMEDCTRouteCodes["Oromucosal use"],
559
683
  "dental paste": types_1.SNOMEDCTRouteCodes["Dental use"],
560
684
  "solution for peritoneal dialysis": types_1.SNOMEDCTRouteCodes["Intradialytic route"],
package/dist/parser.js CHANGED
@@ -82,6 +82,7 @@ const SITE_FILLER_WORDS = new Set([
82
82
  "their",
83
83
  "my"
84
84
  ]);
85
+ const HOUSEHOLD_VOLUME_UNIT_SET = new Set(maps_1.HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase()));
85
86
  const OCULAR_DIRECTION_WORDS = new Set([
86
87
  "left",
87
88
  "right",
@@ -589,6 +590,84 @@ function removeWhen(target, code) {
589
590
  index = target.indexOf(code);
590
591
  }
591
592
  }
593
+ const DEFAULT_EVENT_TIMING_WEIGHTS = {
594
+ [types_1.EventTiming.Immediate]: 0,
595
+ [types_1.EventTiming.Wake]: 6 * 3600,
596
+ [types_1.EventTiming["After Sleep"]]: 6 * 3600 + 15 * 60,
597
+ [types_1.EventTiming["Early Morning"]]: 7 * 3600,
598
+ [types_1.EventTiming["Before Meal"]]: 7 * 3600 + 30 * 60,
599
+ [types_1.EventTiming["Before Breakfast"]]: 7 * 3600 + 45 * 60,
600
+ [types_1.EventTiming.Morning]: 8 * 3600,
601
+ [types_1.EventTiming.Breakfast]: 8 * 3600 + 15 * 60,
602
+ [types_1.EventTiming.Meal]: 8 * 3600 + 30 * 60,
603
+ [types_1.EventTiming["After Breakfast"]]: 9 * 3600,
604
+ [types_1.EventTiming["After Meal"]]: 9 * 3600 + 15 * 60,
605
+ [types_1.EventTiming["Late Morning"]]: 10 * 3600 + 30 * 60,
606
+ [types_1.EventTiming["Before Lunch"]]: 11 * 3600 + 45 * 60,
607
+ [types_1.EventTiming.Noon]: 12 * 3600,
608
+ [types_1.EventTiming.Lunch]: 12 * 3600 + 15 * 60,
609
+ [types_1.EventTiming["After Lunch"]]: 12 * 3600 + 45 * 60,
610
+ [types_1.EventTiming["Early Afternoon"]]: 13 * 3600 + 30 * 60,
611
+ [types_1.EventTiming.Afternoon]: 15 * 3600,
612
+ [types_1.EventTiming["Late Afternoon"]]: 16 * 3600 + 30 * 60,
613
+ [types_1.EventTiming["Before Dinner"]]: 17 * 3600 + 30 * 60,
614
+ [types_1.EventTiming.Dinner]: 18 * 3600,
615
+ [types_1.EventTiming["After Dinner"]]: 19 * 3600,
616
+ [types_1.EventTiming["Early Evening"]]: 19 * 3600 + 30 * 60,
617
+ [types_1.EventTiming.Evening]: 20 * 3600,
618
+ [types_1.EventTiming["Late Evening"]]: 21 * 3600,
619
+ [types_1.EventTiming.Night]: 22 * 3600,
620
+ [types_1.EventTiming["Before Sleep"]]: 22 * 3600 + 30 * 60,
621
+ };
622
+ function parseClockToSeconds(clock) {
623
+ const match = clock.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
624
+ if (!match) {
625
+ return undefined;
626
+ }
627
+ const hour = Number(match[1]);
628
+ const minute = Number(match[2]);
629
+ const second = match[3] ? Number(match[3]) : 0;
630
+ if (!Number.isFinite(hour) ||
631
+ !Number.isFinite(minute) ||
632
+ !Number.isFinite(second) ||
633
+ hour < 0 ||
634
+ hour > 23 ||
635
+ minute < 0 ||
636
+ minute > 59 ||
637
+ second < 0 ||
638
+ second > 59) {
639
+ return undefined;
640
+ }
641
+ return hour * 3600 + minute * 60 + second;
642
+ }
643
+ function computeWhenWeight(code, options) {
644
+ var _a, _b;
645
+ const clock = (_a = options === null || options === void 0 ? void 0 : options.eventClock) === null || _a === void 0 ? void 0 : _a[code];
646
+ if (clock) {
647
+ const seconds = parseClockToSeconds(clock);
648
+ if (seconds !== undefined) {
649
+ return seconds;
650
+ }
651
+ }
652
+ return (_b = DEFAULT_EVENT_TIMING_WEIGHTS[code]) !== null && _b !== void 0 ? _b : 10000;
653
+ }
654
+ function sortWhenValues(internal, options) {
655
+ if (internal.when.length < 2) {
656
+ return;
657
+ }
658
+ const weighted = internal.when.map((code, index) => ({
659
+ code,
660
+ weight: computeWhenWeight(code, options),
661
+ index,
662
+ }));
663
+ weighted.sort((a, b) => {
664
+ if (a.weight !== b.weight) {
665
+ return a.weight - b.weight;
666
+ }
667
+ return a.index - b.index;
668
+ });
669
+ internal.when.splice(0, internal.when.length, ...weighted.map((entry) => entry.code));
670
+ }
592
671
  // Translate the requested expansion context into the appropriate sequence of
593
672
  // EventTiming values (e.g., AC -> ACM/ACD/ACV) for the detected frequency.
594
673
  function computeMealExpansions(base, frequency, pairPreference) {
@@ -653,6 +732,37 @@ function computeMealExpansions(base, frequency, pairPreference) {
653
732
  }
654
733
  return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner, bedtime];
655
734
  }
735
+ function reconcileMealTimingSpecificity(internal) {
736
+ if (!internal.when.length) {
737
+ return;
738
+ }
739
+ const convertSpecifics = (base, mappings) => {
740
+ if (!(0, array_1.arrayIncludes)(internal.when, base)) {
741
+ return;
742
+ }
743
+ let replaced = false;
744
+ for (const [general, specific] of mappings) {
745
+ if ((0, array_1.arrayIncludes)(internal.when, general)) {
746
+ removeWhen(internal.when, general);
747
+ addWhen(internal.when, specific);
748
+ replaced = true;
749
+ }
750
+ }
751
+ if (replaced) {
752
+ removeWhen(internal.when, base);
753
+ }
754
+ };
755
+ convertSpecifics(types_1.EventTiming["Before Meal"], [
756
+ [types_1.EventTiming.Breakfast, types_1.EventTiming["Before Breakfast"]],
757
+ [types_1.EventTiming.Lunch, types_1.EventTiming["Before Lunch"]],
758
+ [types_1.EventTiming.Dinner, types_1.EventTiming["Before Dinner"]],
759
+ ]);
760
+ convertSpecifics(types_1.EventTiming["After Meal"], [
761
+ [types_1.EventTiming.Breakfast, types_1.EventTiming["After Breakfast"]],
762
+ [types_1.EventTiming.Lunch, types_1.EventTiming["After Lunch"]],
763
+ [types_1.EventTiming.Dinner, types_1.EventTiming["After Dinner"]],
764
+ ]);
765
+ }
656
766
  // Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
657
767
  // EventTiming codes when the cadence makes the intent obvious.
658
768
  function expandMealTimings(internal, options) {
@@ -1225,10 +1335,10 @@ function parseInternal(input, options) {
1225
1335
  }
1226
1336
  }
1227
1337
  if (internal.unit === undefined) {
1228
- internal.unit = (0, context_1.inferUnitFromContext)(context);
1338
+ internal.unit = enforceHouseholdUnitPolicy((0, context_1.inferUnitFromContext)(context), options);
1229
1339
  }
1230
1340
  if (internal.unit === undefined) {
1231
- const fallbackUnit = inferUnitFromRouteHints(internal);
1341
+ const fallbackUnit = enforceHouseholdUnitPolicy(inferUnitFromRouteHints(internal), options);
1232
1342
  if (fallbackUnit) {
1233
1343
  internal.unit = fallbackUnit;
1234
1344
  }
@@ -1269,8 +1379,10 @@ function parseInternal(input, options) {
1269
1379
  internal.timingCode = "QID";
1270
1380
  }
1271
1381
  }
1382
+ reconcileMealTimingSpecificity(internal);
1272
1383
  // Expand generic meal markers into specific EventTiming codes when asked to.
1273
1384
  expandMealTimings(internal, options);
1385
+ sortWhenValues(internal, options);
1274
1386
  // Determine site text from leftover tokens (excluding PRN reason tokens)
1275
1387
  const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
1276
1388
  const siteCandidateIndices = new Set();
@@ -1369,16 +1481,24 @@ function parseInternal(input, options) {
1369
1481
  }
1370
1482
  function normalizeUnit(token, options) {
1371
1483
  var _a;
1372
- const override = (_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token];
1484
+ const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
1373
1485
  if (override) {
1374
1486
  return override;
1375
1487
  }
1376
- const defaultUnit = maps_1.DEFAULT_UNIT_SYNONYMS[token];
1488
+ const defaultUnit = enforceHouseholdUnitPolicy(maps_1.DEFAULT_UNIT_SYNONYMS[token], options);
1377
1489
  if (defaultUnit) {
1378
1490
  return defaultUnit;
1379
1491
  }
1380
1492
  return undefined;
1381
1493
  }
1494
+ function enforceHouseholdUnitPolicy(unit, options) {
1495
+ if (unit &&
1496
+ (options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
1497
+ HOUSEHOLD_VOLUME_UNIT_SET.has(unit.toLowerCase())) {
1498
+ return undefined;
1499
+ }
1500
+ return unit;
1501
+ }
1382
1502
  function inferUnitFromRouteHints(internal) {
1383
1503
  if (internal.routeCode) {
1384
1504
  const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[internal.routeCode];
package/dist/suggest.js CHANGED
@@ -2,50 +2,123 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.suggestSig = suggestSig;
4
4
  const context_1 = require("./context");
5
+ const maps_1 = require("./maps");
6
+ const types_1 = require("./types");
5
7
  const DEFAULT_LIMIT = 10;
8
+ const HOUSEHOLD_VOLUME_UNIT_SET = new Set(maps_1.HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.trim().toLowerCase()));
9
+ const ROUTE_TOKEN_BY_CODE = {
10
+ [types_1.RouteCode["Oral route"]]: "po",
11
+ [types_1.RouteCode["Respiratory tract route (qualifier value)"]]: "inh",
12
+ [types_1.RouteCode["Nasal route"]]: "in",
13
+ [types_1.RouteCode["Ophthalmic route"]]: "oph",
14
+ [types_1.RouteCode["Per rectum"]]: "pr",
15
+ [types_1.RouteCode["Transdermal route"]]: "transdermal",
16
+ [types_1.RouteCode["Topical route"]]: "topical",
17
+ };
6
18
  const DEFAULT_UNIT_ROUTE_ORDER = [
7
- { unit: "tab", route: "po" },
8
- { unit: "cap", route: "po" },
9
- { unit: "mL", route: "po" },
10
- { unit: "mg", route: "po" },
11
- { unit: "puff", route: "inh" },
12
- { unit: "spray", route: "in" },
13
- { unit: "drop", route: "oph" },
14
- { unit: "suppository", route: "pr" },
15
- { unit: "patch", route: "transdermal" },
16
- { unit: "g", route: "topical" }
19
+ { unit: "tab", routeCode: types_1.RouteCode["Oral route"] },
20
+ { unit: "cap", routeCode: types_1.RouteCode["Oral route"] },
21
+ { unit: "tsp", routeCode: types_1.RouteCode["Oral route"] },
22
+ { unit: "tbsp", routeCode: types_1.RouteCode["Oral route"] },
23
+ { unit: "mL", routeCode: types_1.RouteCode["Oral route"] },
24
+ { unit: "L", routeCode: types_1.RouteCode["Oral route"] },
25
+ { unit: "mcL", routeCode: types_1.RouteCode["Oral route"] },
26
+ { unit: "nL", routeCode: types_1.RouteCode["Oral route"] },
27
+ { unit: "mg", routeCode: types_1.RouteCode["Oral route"] },
28
+ { unit: "mcg", routeCode: types_1.RouteCode["Oral route"] },
29
+ { unit: "ng", routeCode: types_1.RouteCode["Oral route"] },
30
+ { unit: "g", routeCode: types_1.RouteCode["Topical route"] },
31
+ { unit: "kg", routeCode: types_1.RouteCode["Topical route"] },
32
+ { unit: "puff", routeCode: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
33
+ { unit: "spray", routeCode: types_1.RouteCode["Nasal route"] },
34
+ { unit: "drop", routeCode: types_1.RouteCode["Ophthalmic route"] },
35
+ { unit: "suppository", routeCode: types_1.RouteCode["Per rectum"] },
36
+ { unit: "patch", routeCode: types_1.RouteCode["Transdermal route"] },
17
37
  ];
18
- const DEFAULT_ROUTE_BY_UNIT = {
19
- tab: "po",
20
- tabs: "po",
21
- tablet: "po",
22
- cap: "po",
23
- capsule: "po",
24
- ml: "po",
25
- mg: "po",
26
- puff: "inh",
27
- puffs: "inh",
28
- spray: "in",
29
- sprays: "in",
30
- drop: "oph",
31
- drops: "oph",
32
- suppository: "pr",
33
- suppositories: "pr",
34
- patch: "transdermal",
35
- patches: "transdermal",
36
- g: "topical"
37
- };
38
- const FREQUENCY_CODES = ["qd", "bid", "tid", "qid"];
39
- const INTERVAL_CODES = ["q4h", "q6h", "q8h"];
40
- const WHEN_TOKENS = ["ac", "pc", "hs", "am", "pm"];
41
- const CORE_WHEN_TOKENS = ["pc", "ac", "hs"];
42
- const FREQUENCY_NUMBERS = [1, 2, 3, 4];
43
- const FREQ_TOKEN_BY_NUMBER = {
44
- 1: "qd",
45
- 2: "bid",
46
- 3: "tid",
47
- 4: "qid",
48
- };
38
+ const ROUTE_TOKEN_BY_UNIT = (() => {
39
+ var _a, _b, _c;
40
+ const map = new Map();
41
+ const assign = (unit, token) => {
42
+ if (!unit || !token) {
43
+ return;
44
+ }
45
+ const normalizedUnit = normalizeKey(unit);
46
+ if (!normalizedUnit || map.has(normalizedUnit)) {
47
+ return;
48
+ }
49
+ map.set(normalizedUnit, normalizeSpacing(token));
50
+ };
51
+ for (const routeCodeKey in maps_1.DEFAULT_UNIT_BY_ROUTE) {
52
+ if (!Object.prototype.hasOwnProperty.call(maps_1.DEFAULT_UNIT_BY_ROUTE, routeCodeKey)) {
53
+ continue;
54
+ }
55
+ const routeCode = routeCodeKey;
56
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[routeCode];
57
+ if (!unit) {
58
+ continue;
59
+ }
60
+ const token = (_a = ROUTE_TOKEN_BY_CODE[routeCode]) !== null && _a !== void 0 ? _a : maps_1.ROUTE_TEXT[routeCode];
61
+ assign(unit, token);
62
+ }
63
+ for (const preference of DEFAULT_UNIT_ROUTE_ORDER) {
64
+ const token = (_c = (_b = preference.routeToken) !== null && _b !== void 0 ? _b : ROUTE_TOKEN_BY_CODE[preference.routeCode]) !== null && _c !== void 0 ? _c : maps_1.ROUTE_TEXT[preference.routeCode];
65
+ assign(preference.unit, token);
66
+ }
67
+ return map;
68
+ })();
69
+ const BASE_INTERVAL_CODES = Object.keys(maps_1.TIMING_ABBREVIATIONS)
70
+ .filter((token) => /^q\d+h$/.test(token))
71
+ .sort((a, b) => Number.parseInt(a.slice(1, -1), 10) - Number.parseInt(b.slice(1, -1), 10));
72
+ const DEFAULT_INTERVAL_RANGES = ["q2-4h", "q4-6h", "q6-8h", "q8-12h"];
73
+ const BASE_WHEN_TOKEN_CANDIDATES = [
74
+ "ac",
75
+ "pc",
76
+ "hs",
77
+ "am",
78
+ "pm",
79
+ "morn",
80
+ "morning",
81
+ "noon",
82
+ "afternoon",
83
+ "evening",
84
+ "night",
85
+ "bedtime",
86
+ "wake",
87
+ "waking",
88
+ "breakfast",
89
+ "lunch",
90
+ "dinner",
91
+ "stat",
92
+ ];
93
+ const WHEN_TOKENS = BASE_WHEN_TOKEN_CANDIDATES.filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
94
+ const WHEN_COMBINATIONS = [
95
+ "am",
96
+ "morning",
97
+ "morn",
98
+ "noon",
99
+ "afternoon",
100
+ "pm",
101
+ "evening",
102
+ "night",
103
+ "hs",
104
+ "bedtime",
105
+ ].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
106
+ const CORE_WHEN_TOKENS = ["pc", "ac", "hs"].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
107
+ const FREQUENCY_CODES = ["qd", "od", "bid", "tid", "qid"].filter((token) => maps_1.TIMING_ABBREVIATIONS[token] !== undefined);
108
+ const FREQ_TOKEN_BY_NUMBER = {};
109
+ for (const [frequency, token] of [
110
+ [1, "qd"],
111
+ [2, "bid"],
112
+ [3, "tid"],
113
+ [4, "qid"],
114
+ ]) {
115
+ if (maps_1.TIMING_ABBREVIATIONS[token]) {
116
+ FREQ_TOKEN_BY_NUMBER[frequency] = token;
117
+ }
118
+ }
119
+ const FREQUENCY_NUMBERS = Object.keys(FREQ_TOKEN_BY_NUMBER)
120
+ .map((value) => Number.parseInt(value, 10))
121
+ .sort((a, b) => a - b);
49
122
  const DEFAULT_PRN_REASONS = [
50
123
  "pain",
51
124
  "nausea",
@@ -59,6 +132,72 @@ const DEFAULT_PRN_REASONS = [
59
132
  "dyspnea",
60
133
  ];
61
134
  const DEFAULT_DOSE_COUNTS = ["1", "2"];
135
+ const OPTIONAL_MATCH_TOKENS = new Set([
136
+ "to",
137
+ "into",
138
+ "in",
139
+ "on",
140
+ "onto",
141
+ "per",
142
+ "for",
143
+ "the",
144
+ "od",
145
+ "os",
146
+ "ou",
147
+ ]);
148
+ const ROUTE_TOKEN_FRAGMENTS = new Set();
149
+ for (const phrase of Object.keys(maps_1.DEFAULT_ROUTE_SYNONYMS)) {
150
+ for (const fragment of phrase.split(/\s+/)) {
151
+ const normalized = fragment.trim();
152
+ if (normalized) {
153
+ ROUTE_TOKEN_FRAGMENTS.add(normalized);
154
+ }
155
+ }
156
+ }
157
+ const SKIPPABLE_CANDIDATE_TOKENS = new Set([
158
+ ...Array.from(OPTIONAL_MATCH_TOKENS),
159
+ ...Array.from(ROUTE_TOKEN_FRAGMENTS),
160
+ ]);
161
+ const UNIT_LOOKUP = (() => {
162
+ const canonicalByKey = new Map();
163
+ const variantsByCanonical = new Map();
164
+ const registerVariant = (canonical, variant) => {
165
+ const normalizedCanonical = normalizeKey(canonical);
166
+ if (!normalizedCanonical) {
167
+ return;
168
+ }
169
+ let variants = variantsByCanonical.get(normalizedCanonical);
170
+ if (!variants) {
171
+ variants = new Set();
172
+ variantsByCanonical.set(normalizedCanonical, variants);
173
+ }
174
+ variants.add(normalizeSpacing(canonical));
175
+ variants.add(normalizeSpacing(variant));
176
+ };
177
+ for (const token in maps_1.DEFAULT_UNIT_SYNONYMS) {
178
+ if (!Object.prototype.hasOwnProperty.call(maps_1.DEFAULT_UNIT_SYNONYMS, token)) {
179
+ continue;
180
+ }
181
+ const canonicalValue = maps_1.DEFAULT_UNIT_SYNONYMS[token];
182
+ const canonical = normalizeSpacing(canonicalValue);
183
+ registerVariant(canonical, canonical);
184
+ registerVariant(canonical, token);
185
+ canonicalByKey.set(normalizeKey(token), canonical);
186
+ canonicalByKey.set(normalizeKey(canonical), canonical);
187
+ }
188
+ return { canonicalByKey, variantsByCanonical };
189
+ })();
190
+ function resolveCanonicalUnit(unit) {
191
+ var _a;
192
+ if (!unit) {
193
+ return undefined;
194
+ }
195
+ const normalized = normalizeKey(unit);
196
+ if (!normalized) {
197
+ return undefined;
198
+ }
199
+ return (_a = UNIT_LOOKUP.canonicalByKey.get(normalized)) !== null && _a !== void 0 ? _a : normalizeSpacing(unit);
200
+ }
62
201
  function normalizeKey(value) {
63
202
  return value.trim().toLowerCase();
64
203
  }
@@ -67,33 +206,153 @@ function normalizeSpacing(value) {
67
206
  .trim()
68
207
  .replace(/\s+/g, " ");
69
208
  }
70
- function buildUnitRoutePairs(contextUnit) {
209
+ function getUnitVariants(unit) {
210
+ var _a;
211
+ const canonical = (_a = resolveCanonicalUnit(unit)) !== null && _a !== void 0 ? _a : normalizeSpacing(unit);
212
+ const normalizedCanonical = normalizeKey(canonical);
213
+ const variants = new Set();
214
+ const push = (candidate) => {
215
+ if (!candidate) {
216
+ return;
217
+ }
218
+ const normalizedCandidate = normalizeSpacing(candidate);
219
+ if (!normalizedCandidate) {
220
+ return;
221
+ }
222
+ variants.add(normalizedCandidate);
223
+ };
224
+ push(canonical);
225
+ push(unit);
226
+ const canonicalVariants = UNIT_LOOKUP.variantsByCanonical.get(normalizedCanonical);
227
+ if (canonicalVariants) {
228
+ for (const candidate of canonicalVariants) {
229
+ push(candidate);
230
+ }
231
+ }
232
+ return [...variants];
233
+ }
234
+ function buildIntervalTokens(input) {
235
+ const intervals = new Set();
236
+ const add = (token) => {
237
+ if (!token) {
238
+ return;
239
+ }
240
+ const normalized = token.trim().toLowerCase();
241
+ if (!normalized) {
242
+ return;
243
+ }
244
+ intervals.add(normalized);
245
+ };
246
+ for (const token of BASE_INTERVAL_CODES) {
247
+ add(token);
248
+ }
249
+ for (const token of DEFAULT_INTERVAL_RANGES) {
250
+ add(token);
251
+ }
252
+ const normalizedInput = input.toLowerCase();
253
+ const rawTokens = normalizedInput.split(/[^a-z0-9-]+/g);
254
+ for (const rawToken of rawTokens) {
255
+ if (!rawToken) {
256
+ continue;
257
+ }
258
+ const match = rawToken.match(/^q(\d{1,2})(?:-(\d{1,2}))?(h?)$/);
259
+ if (!match) {
260
+ continue;
261
+ }
262
+ const first = Number.parseInt(match[1], 10);
263
+ const second = match[2] ? Number.parseInt(match[2], 10) : undefined;
264
+ if (Number.isNaN(first) || first <= 0 || first > 48) {
265
+ continue;
266
+ }
267
+ if (second !== undefined) {
268
+ if (Number.isNaN(second) || second < first || second > 48) {
269
+ continue;
270
+ }
271
+ }
272
+ const normalized = `q${first}${second ? `-${second}` : ""}h`;
273
+ add(normalized);
274
+ }
275
+ return [...intervals];
276
+ }
277
+ function buildWhenSequences() {
278
+ const sequences = [];
279
+ for (const token of WHEN_TOKENS) {
280
+ sequences.push([token]);
281
+ }
282
+ for (let i = 0; i < WHEN_COMBINATIONS.length; i++) {
283
+ const first = WHEN_COMBINATIONS[i];
284
+ for (let j = i + 1; j < WHEN_COMBINATIONS.length; j++) {
285
+ const second = WHEN_COMBINATIONS[j];
286
+ sequences.push([first, second]);
287
+ }
288
+ }
289
+ return sequences;
290
+ }
291
+ function tokenizeForMatching(value) {
292
+ return value
293
+ .toLowerCase()
294
+ .split(/\s+/)
295
+ .map((token) => token.replace(/^[^a-z0-9-]+|[^a-z0-9-]+$/g, ""))
296
+ .filter((token) => token.length > 0)
297
+ .filter((token) => !OPTIONAL_MATCH_TOKENS.has(token));
298
+ }
299
+ function canonicalizeForMatching(value) {
300
+ return tokenizeForMatching(value).join(" ");
301
+ }
302
+ function tokensMatch(prefixTokens, candidateTokens) {
303
+ if (prefixTokens.length === 0) {
304
+ return true;
305
+ }
306
+ let prefixIndex = 0;
307
+ for (const candidateToken of candidateTokens) {
308
+ if (prefixIndex >= prefixTokens.length) {
309
+ return true;
310
+ }
311
+ const prefixToken = prefixTokens[prefixIndex];
312
+ if (candidateToken.startsWith(prefixToken)) {
313
+ prefixIndex += 1;
314
+ if (prefixIndex >= prefixTokens.length) {
315
+ return true;
316
+ }
317
+ continue;
318
+ }
319
+ if (!SKIPPABLE_CANDIDATE_TOKENS.has(candidateToken)) {
320
+ return false;
321
+ }
322
+ }
323
+ return prefixIndex >= prefixTokens.length;
324
+ }
325
+ function buildUnitRoutePairs(contextUnit, options) {
326
+ var _a, _b;
71
327
  const pairs = [];
72
328
  const seen = new Set();
73
- const addPair = (unit, route) => {
329
+ const addPair = (unit, routeOverride) => {
74
330
  var _a;
75
- if (!unit) {
331
+ const canonicalUnit = resolveCanonicalUnit(unit);
332
+ if (!canonicalUnit) {
333
+ return;
334
+ }
335
+ const normalizedUnit = normalizeKey(canonicalUnit);
336
+ if ((options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
337
+ HOUSEHOLD_VOLUME_UNIT_SET.has(normalizedUnit)) {
76
338
  return;
77
339
  }
78
- const cleanUnit = unit.trim();
79
- if (!cleanUnit) {
340
+ const resolvedRoute = (_a = routeOverride !== null && routeOverride !== void 0 ? routeOverride : ROUTE_TOKEN_BY_UNIT.get(normalizedUnit)) !== null && _a !== void 0 ? _a : "po";
341
+ const cleanRoute = normalizeSpacing(resolvedRoute);
342
+ if (!cleanRoute) {
80
343
  return;
81
344
  }
82
- const normalizedUnit = cleanUnit.toLowerCase();
83
- const resolvedRoute = (_a = route !== null && route !== void 0 ? route : DEFAULT_ROUTE_BY_UNIT[normalizedUnit]) !== null && _a !== void 0 ? _a : "po";
84
- const key = `${normalizedUnit}::${resolvedRoute.toLowerCase()}`;
345
+ const key = `${normalizedUnit}::${normalizeKey(cleanRoute)}`;
85
346
  if (seen.has(key)) {
86
347
  return;
87
348
  }
88
349
  seen.add(key);
89
- pairs.push({ unit: cleanUnit, route: resolvedRoute });
350
+ pairs.push({ unit: canonicalUnit, route: cleanRoute });
90
351
  };
91
- if (contextUnit) {
92
- const normalized = normalizeKey(contextUnit);
93
- addPair(contextUnit, DEFAULT_ROUTE_BY_UNIT[normalized]);
94
- }
95
- for (const pair of DEFAULT_UNIT_ROUTE_ORDER) {
96
- addPair(pair.unit, pair.route);
352
+ addPair(contextUnit);
353
+ for (const preference of DEFAULT_UNIT_ROUTE_ORDER) {
354
+ const routeToken = (_b = (_a = preference.routeToken) !== null && _a !== void 0 ? _a : ROUTE_TOKEN_BY_CODE[preference.routeCode]) !== null && _b !== void 0 ? _b : maps_1.ROUTE_TEXT[preference.routeCode];
355
+ addPair(preference.unit, routeToken);
97
356
  }
98
357
  return pairs;
99
358
  }
@@ -144,7 +403,7 @@ function buildDoseValues(input) {
144
403
  }
145
404
  return [...values];
146
405
  }
147
- function generateCandidateSignatures(pairs, doseValues, prnReasons) {
406
+ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences) {
148
407
  const suggestions = [];
149
408
  const seen = new Set();
150
409
  const push = (value) => {
@@ -160,41 +419,57 @@ function generateCandidateSignatures(pairs, doseValues, prnReasons) {
160
419
  suggestions.push(normalized);
161
420
  };
162
421
  for (const pair of pairs) {
422
+ const unitVariants = getUnitVariants(pair.unit);
163
423
  for (const code of FREQUENCY_CODES) {
164
- for (const dose of doseValues) {
165
- push(`${dose} ${pair.unit} ${pair.route} ${code}`);
424
+ for (const unitVariant of unitVariants) {
425
+ for (const dose of doseValues) {
426
+ push(`${dose} ${unitVariant} ${pair.route} ${code}`);
427
+ }
166
428
  }
167
429
  push(`${pair.route} ${code}`);
168
430
  }
169
- for (const interval of INTERVAL_CODES) {
170
- for (const dose of doseValues) {
171
- push(`${dose} ${pair.unit} ${pair.route} ${interval}`);
172
- for (const reason of prnReasons) {
173
- push(`${dose} ${pair.unit} ${pair.route} ${interval} prn ${reason}`);
431
+ for (const interval of intervalTokens) {
432
+ for (const unitVariant of unitVariants) {
433
+ for (const dose of doseValues) {
434
+ push(`${dose} ${unitVariant} ${pair.route} ${interval}`);
435
+ for (const reason of prnReasons) {
436
+ push(`${dose} ${unitVariant} ${pair.route} ${interval} prn ${reason}`);
437
+ }
174
438
  }
175
439
  }
176
440
  push(`${pair.route} ${interval}`);
177
441
  }
178
442
  for (const freq of FREQUENCY_NUMBERS) {
179
443
  const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
444
+ if (!freqToken) {
445
+ continue;
446
+ }
180
447
  push(`1x${freq} ${pair.route} ${freqToken}`);
181
448
  for (const when of CORE_WHEN_TOKENS) {
182
449
  push(`1x${freq} ${pair.route} ${when}`);
183
450
  }
184
451
  }
185
- for (const when of WHEN_TOKENS) {
186
- for (const dose of doseValues) {
187
- push(`${dose} ${pair.unit} ${pair.route} ${when}`);
452
+ for (const whenSequence of whenSequences) {
453
+ const suffix = whenSequence.join(" ");
454
+ for (const unitVariant of unitVariants) {
455
+ for (const dose of doseValues) {
456
+ push(`${dose} ${unitVariant} ${pair.route} ${suffix}`);
457
+ }
188
458
  }
189
- push(`${pair.route} ${when}`);
459
+ push(`${pair.route} ${suffix}`);
190
460
  }
191
461
  for (const reason of prnReasons) {
192
- push(`1 ${pair.unit} ${pair.route} prn ${reason}`);
462
+ for (const unitVariant of unitVariants) {
463
+ for (const dose of doseValues) {
464
+ push(`${dose} ${unitVariant} ${pair.route} prn ${reason}`);
465
+ }
466
+ }
467
+ push(`${pair.route} prn ${reason}`);
193
468
  }
194
469
  }
195
470
  return suggestions;
196
471
  }
197
- function matchesPrefix(candidate, prefix, prefixCompact) {
472
+ function matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes) {
198
473
  if (!prefix) {
199
474
  return true;
200
475
  }
@@ -203,21 +478,53 @@ function matchesPrefix(candidate, prefix, prefixCompact) {
203
478
  return true;
204
479
  }
205
480
  const compactCandidate = normalizedCandidate.replace(/\s+/g, "");
206
- return compactCandidate.startsWith(prefixCompact);
481
+ if (compactCandidate.startsWith(prefixCompact)) {
482
+ return true;
483
+ }
484
+ const candidateNoDashes = normalizedCandidate.replace(/-/g, "");
485
+ if (candidateNoDashes.startsWith(prefixNoDashes)) {
486
+ return true;
487
+ }
488
+ const canonicalCandidate = canonicalizeForMatching(candidate);
489
+ if (canonicalCandidate.startsWith(prefixCanonical)) {
490
+ return true;
491
+ }
492
+ const canonicalCompact = canonicalCandidate.replace(/\s+/g, "");
493
+ if (canonicalCompact.startsWith(prefixCanonicalCompact)) {
494
+ return true;
495
+ }
496
+ const candidateTokens = tokenizeForMatching(candidate);
497
+ if (tokensMatch(prefixTokens, candidateTokens)) {
498
+ return true;
499
+ }
500
+ const canonicalNoDashes = canonicalCandidate.replace(/-/g, "");
501
+ if (canonicalNoDashes.startsWith(prefixCanonicalNoDashes)) {
502
+ return true;
503
+ }
504
+ const candidateTokensNoDashes = candidateTokens.map((token) => token.replace(/-/g, ""));
505
+ return tokensMatch(prefixTokensNoDashes, candidateTokensNoDashes);
207
506
  }
208
507
  function suggestSig(input, options) {
209
508
  var _a, _b;
210
509
  const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : DEFAULT_LIMIT;
211
510
  const prefix = normalizeSpacing(input.toLowerCase());
212
511
  const prefixCompact = prefix.replace(/\s+/g, "");
512
+ const prefixNoDashes = prefix.replace(/-/g, "");
513
+ const prefixCanonical = canonicalizeForMatching(prefix);
514
+ const prefixCanonicalCompact = prefixCanonical.replace(/\s+/g, "");
515
+ const prefixCanonicalNoDashes = prefixCanonical.replace(/-/g, "");
516
+ const prefixTokens = tokenizeForMatching(prefixCanonical);
517
+ const prefixTokensNoDashes = prefixTokens.map((token) => token.replace(/-/g, ""));
213
518
  const contextUnit = (0, context_1.inferUnitFromContext)((_b = options === null || options === void 0 ? void 0 : options.context) !== null && _b !== void 0 ? _b : undefined);
214
- const pairs = buildUnitRoutePairs(contextUnit);
519
+ const pairs = buildUnitRoutePairs(contextUnit, options);
215
520
  const doseValues = buildDoseValues(input);
216
521
  const prnReasons = buildPrnReasons(options === null || options === void 0 ? void 0 : options.prnReasons);
217
- const candidates = generateCandidateSignatures(pairs, doseValues, prnReasons);
522
+ const intervalTokens = buildIntervalTokens(input);
523
+ const whenSequences = buildWhenSequences();
524
+ const candidates = generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences);
218
525
  const results = [];
219
526
  for (const candidate of candidates) {
220
- if (matchesPrefix(candidate, prefix, prefixCompact)) {
527
+ if (matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes)) {
221
528
  results.push(candidate);
222
529
  }
223
530
  if (results.length >= limit) {
package/dist/types.d.ts CHANGED
@@ -303,6 +303,11 @@ export interface ParseOptions extends FormatOptions {
303
303
  intervalWeeks?: number;
304
304
  }>;
305
305
  whenMap?: Record<string, EventTiming>;
306
+ /**
307
+ * Allows supplying institution-specific event clock anchors so parsed
308
+ * EventTiming arrays can be ordered chronologically for that locale.
309
+ */
310
+ eventClock?: EventClockMap;
306
311
  allowDiscouraged?: boolean;
307
312
  /**
308
313
  * When enabled the parser will expand generic meal timing tokens (AC/PC/C)
@@ -315,6 +320,11 @@ export interface ParseOptions extends FormatOptions {
315
320
  * Defaults to "breakfast+dinner" to mirror common clinical practice.
316
321
  */
317
322
  twoPerDayPair?: "breakfast+dinner" | "breakfast+lunch";
323
+ /**
324
+ * Allows disabling recognition of household volume units such as teaspoon
325
+ * and tablespoon when set to false. Defaults to true.
326
+ */
327
+ allowHouseholdVolumeUnits?: boolean;
318
328
  }
319
329
  export interface ParseResult {
320
330
  fhir: FhirDosage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",