ezmedicationinput 0.1.2 → 0.1.5
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 +41 -1
- package/dist/maps.d.ts +1 -0
- package/dist/maps.js +140 -16
- package/dist/parser.js +145 -13
- package/dist/suggest.js +417 -89
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
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`
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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",
|
|
@@ -109,6 +110,7 @@ const COMBO_EVENT_TIMINGS = {
|
|
|
109
110
|
"after sleep": types_1.EventTiming["After Sleep"],
|
|
110
111
|
"upon waking": types_1.EventTiming.Wake
|
|
111
112
|
};
|
|
113
|
+
const MEAL_CONTEXT_CONNECTORS = new Set(["and", "or", "&", "+", "plus"]);
|
|
112
114
|
// Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
|
|
113
115
|
// logic bail early when the clinician already specified precise events.
|
|
114
116
|
const SPECIFIC_MEAL_TIMINGS = new Set([
|
|
@@ -589,6 +591,84 @@ function removeWhen(target, code) {
|
|
|
589
591
|
index = target.indexOf(code);
|
|
590
592
|
}
|
|
591
593
|
}
|
|
594
|
+
const DEFAULT_EVENT_TIMING_WEIGHTS = {
|
|
595
|
+
[types_1.EventTiming.Immediate]: 0,
|
|
596
|
+
[types_1.EventTiming.Wake]: 6 * 3600,
|
|
597
|
+
[types_1.EventTiming["After Sleep"]]: 6 * 3600 + 15 * 60,
|
|
598
|
+
[types_1.EventTiming["Early Morning"]]: 7 * 3600,
|
|
599
|
+
[types_1.EventTiming["Before Meal"]]: 7 * 3600 + 30 * 60,
|
|
600
|
+
[types_1.EventTiming["Before Breakfast"]]: 7 * 3600 + 45 * 60,
|
|
601
|
+
[types_1.EventTiming.Morning]: 8 * 3600,
|
|
602
|
+
[types_1.EventTiming.Breakfast]: 8 * 3600 + 15 * 60,
|
|
603
|
+
[types_1.EventTiming.Meal]: 8 * 3600 + 30 * 60,
|
|
604
|
+
[types_1.EventTiming["After Breakfast"]]: 9 * 3600,
|
|
605
|
+
[types_1.EventTiming["After Meal"]]: 9 * 3600 + 15 * 60,
|
|
606
|
+
[types_1.EventTiming["Late Morning"]]: 10 * 3600 + 30 * 60,
|
|
607
|
+
[types_1.EventTiming["Before Lunch"]]: 11 * 3600 + 45 * 60,
|
|
608
|
+
[types_1.EventTiming.Noon]: 12 * 3600,
|
|
609
|
+
[types_1.EventTiming.Lunch]: 12 * 3600 + 15 * 60,
|
|
610
|
+
[types_1.EventTiming["After Lunch"]]: 12 * 3600 + 45 * 60,
|
|
611
|
+
[types_1.EventTiming["Early Afternoon"]]: 13 * 3600 + 30 * 60,
|
|
612
|
+
[types_1.EventTiming.Afternoon]: 15 * 3600,
|
|
613
|
+
[types_1.EventTiming["Late Afternoon"]]: 16 * 3600 + 30 * 60,
|
|
614
|
+
[types_1.EventTiming["Before Dinner"]]: 17 * 3600 + 30 * 60,
|
|
615
|
+
[types_1.EventTiming.Dinner]: 18 * 3600,
|
|
616
|
+
[types_1.EventTiming["After Dinner"]]: 19 * 3600,
|
|
617
|
+
[types_1.EventTiming["Early Evening"]]: 19 * 3600 + 30 * 60,
|
|
618
|
+
[types_1.EventTiming.Evening]: 20 * 3600,
|
|
619
|
+
[types_1.EventTiming["Late Evening"]]: 21 * 3600,
|
|
620
|
+
[types_1.EventTiming.Night]: 22 * 3600,
|
|
621
|
+
[types_1.EventTiming["Before Sleep"]]: 22 * 3600 + 30 * 60,
|
|
622
|
+
};
|
|
623
|
+
function parseClockToSeconds(clock) {
|
|
624
|
+
const match = clock.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
625
|
+
if (!match) {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
const hour = Number(match[1]);
|
|
629
|
+
const minute = Number(match[2]);
|
|
630
|
+
const second = match[3] ? Number(match[3]) : 0;
|
|
631
|
+
if (!Number.isFinite(hour) ||
|
|
632
|
+
!Number.isFinite(minute) ||
|
|
633
|
+
!Number.isFinite(second) ||
|
|
634
|
+
hour < 0 ||
|
|
635
|
+
hour > 23 ||
|
|
636
|
+
minute < 0 ||
|
|
637
|
+
minute > 59 ||
|
|
638
|
+
second < 0 ||
|
|
639
|
+
second > 59) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
return hour * 3600 + minute * 60 + second;
|
|
643
|
+
}
|
|
644
|
+
function computeWhenWeight(code, options) {
|
|
645
|
+
var _a, _b;
|
|
646
|
+
const clock = (_a = options === null || options === void 0 ? void 0 : options.eventClock) === null || _a === void 0 ? void 0 : _a[code];
|
|
647
|
+
if (clock) {
|
|
648
|
+
const seconds = parseClockToSeconds(clock);
|
|
649
|
+
if (seconds !== undefined) {
|
|
650
|
+
return seconds;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return (_b = DEFAULT_EVENT_TIMING_WEIGHTS[code]) !== null && _b !== void 0 ? _b : 10000;
|
|
654
|
+
}
|
|
655
|
+
function sortWhenValues(internal, options) {
|
|
656
|
+
if (internal.when.length < 2) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const weighted = internal.when.map((code, index) => ({
|
|
660
|
+
code,
|
|
661
|
+
weight: computeWhenWeight(code, options),
|
|
662
|
+
index,
|
|
663
|
+
}));
|
|
664
|
+
weighted.sort((a, b) => {
|
|
665
|
+
if (a.weight !== b.weight) {
|
|
666
|
+
return a.weight - b.weight;
|
|
667
|
+
}
|
|
668
|
+
return a.index - b.index;
|
|
669
|
+
});
|
|
670
|
+
internal.when.splice(0, internal.when.length, ...weighted.map((entry) => entry.code));
|
|
671
|
+
}
|
|
592
672
|
// Translate the requested expansion context into the appropriate sequence of
|
|
593
673
|
// EventTiming values (e.g., AC -> ACM/ACD/ACV) for the detected frequency.
|
|
594
674
|
function computeMealExpansions(base, frequency, pairPreference) {
|
|
@@ -653,6 +733,37 @@ function computeMealExpansions(base, frequency, pairPreference) {
|
|
|
653
733
|
}
|
|
654
734
|
return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner, bedtime];
|
|
655
735
|
}
|
|
736
|
+
function reconcileMealTimingSpecificity(internal) {
|
|
737
|
+
if (!internal.when.length) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const convertSpecifics = (base, mappings) => {
|
|
741
|
+
if (!(0, array_1.arrayIncludes)(internal.when, base)) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let replaced = false;
|
|
745
|
+
for (const [general, specific] of mappings) {
|
|
746
|
+
if ((0, array_1.arrayIncludes)(internal.when, general)) {
|
|
747
|
+
removeWhen(internal.when, general);
|
|
748
|
+
addWhen(internal.when, specific);
|
|
749
|
+
replaced = true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (replaced) {
|
|
753
|
+
removeWhen(internal.when, base);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
convertSpecifics(types_1.EventTiming["Before Meal"], [
|
|
757
|
+
[types_1.EventTiming.Breakfast, types_1.EventTiming["Before Breakfast"]],
|
|
758
|
+
[types_1.EventTiming.Lunch, types_1.EventTiming["Before Lunch"]],
|
|
759
|
+
[types_1.EventTiming.Dinner, types_1.EventTiming["Before Dinner"]],
|
|
760
|
+
]);
|
|
761
|
+
convertSpecifics(types_1.EventTiming["After Meal"], [
|
|
762
|
+
[types_1.EventTiming.Breakfast, types_1.EventTiming["After Breakfast"]],
|
|
763
|
+
[types_1.EventTiming.Lunch, types_1.EventTiming["After Lunch"]],
|
|
764
|
+
[types_1.EventTiming.Dinner, types_1.EventTiming["After Dinner"]],
|
|
765
|
+
]);
|
|
766
|
+
}
|
|
656
767
|
// Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
|
|
657
768
|
// EventTiming codes when the cadence makes the intent obvious.
|
|
658
769
|
function expandMealTimings(internal, options) {
|
|
@@ -866,20 +977,31 @@ function applyWhenToken(internal, token, code) {
|
|
|
866
977
|
}
|
|
867
978
|
function parseMealContext(internal, tokens, index, code) {
|
|
868
979
|
const token = tokens[index];
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
980
|
+
let converted = 0;
|
|
981
|
+
for (let lookahead = index + 1; lookahead < tokens.length; lookahead++) {
|
|
982
|
+
const nextToken = tokens[lookahead];
|
|
983
|
+
if (internal.consumed.has(nextToken.index)) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (MEAL_CONTEXT_CONNECTORS.has(nextToken.lower)) {
|
|
987
|
+
mark(internal.consumed, nextToken);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const meal = maps_1.MEAL_KEYWORDS[nextToken.lower];
|
|
991
|
+
if (!meal) {
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
876
994
|
const whenCode = code === types_1.EventTiming["After Meal"]
|
|
877
995
|
? meal.pc
|
|
878
996
|
: code === types_1.EventTiming["Before Meal"]
|
|
879
997
|
? meal.ac
|
|
880
998
|
: code;
|
|
881
|
-
|
|
882
|
-
mark(internal.consumed,
|
|
999
|
+
addWhen(internal.when, whenCode);
|
|
1000
|
+
mark(internal.consumed, nextToken);
|
|
1001
|
+
converted++;
|
|
1002
|
+
}
|
|
1003
|
+
if (converted > 0) {
|
|
1004
|
+
mark(internal.consumed, token);
|
|
883
1005
|
return;
|
|
884
1006
|
}
|
|
885
1007
|
applyWhenToken(internal, token, code);
|
|
@@ -1225,10 +1347,10 @@ function parseInternal(input, options) {
|
|
|
1225
1347
|
}
|
|
1226
1348
|
}
|
|
1227
1349
|
if (internal.unit === undefined) {
|
|
1228
|
-
internal.unit = (0, context_1.inferUnitFromContext)(context);
|
|
1350
|
+
internal.unit = enforceHouseholdUnitPolicy((0, context_1.inferUnitFromContext)(context), options);
|
|
1229
1351
|
}
|
|
1230
1352
|
if (internal.unit === undefined) {
|
|
1231
|
-
const fallbackUnit = inferUnitFromRouteHints(internal);
|
|
1353
|
+
const fallbackUnit = enforceHouseholdUnitPolicy(inferUnitFromRouteHints(internal), options);
|
|
1232
1354
|
if (fallbackUnit) {
|
|
1233
1355
|
internal.unit = fallbackUnit;
|
|
1234
1356
|
}
|
|
@@ -1269,8 +1391,10 @@ function parseInternal(input, options) {
|
|
|
1269
1391
|
internal.timingCode = "QID";
|
|
1270
1392
|
}
|
|
1271
1393
|
}
|
|
1394
|
+
reconcileMealTimingSpecificity(internal);
|
|
1272
1395
|
// Expand generic meal markers into specific EventTiming codes when asked to.
|
|
1273
1396
|
expandMealTimings(internal, options);
|
|
1397
|
+
sortWhenValues(internal, options);
|
|
1274
1398
|
// Determine site text from leftover tokens (excluding PRN reason tokens)
|
|
1275
1399
|
const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
|
|
1276
1400
|
const siteCandidateIndices = new Set();
|
|
@@ -1369,16 +1493,24 @@ function parseInternal(input, options) {
|
|
|
1369
1493
|
}
|
|
1370
1494
|
function normalizeUnit(token, options) {
|
|
1371
1495
|
var _a;
|
|
1372
|
-
const override = (_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token];
|
|
1496
|
+
const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
|
|
1373
1497
|
if (override) {
|
|
1374
1498
|
return override;
|
|
1375
1499
|
}
|
|
1376
|
-
const defaultUnit = maps_1.DEFAULT_UNIT_SYNONYMS[token];
|
|
1500
|
+
const defaultUnit = enforceHouseholdUnitPolicy(maps_1.DEFAULT_UNIT_SYNONYMS[token], options);
|
|
1377
1501
|
if (defaultUnit) {
|
|
1378
1502
|
return defaultUnit;
|
|
1379
1503
|
}
|
|
1380
1504
|
return undefined;
|
|
1381
1505
|
}
|
|
1506
|
+
function enforceHouseholdUnitPolicy(unit, options) {
|
|
1507
|
+
if (unit &&
|
|
1508
|
+
(options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
|
|
1509
|
+
HOUSEHOLD_VOLUME_UNIT_SET.has(unit.toLowerCase())) {
|
|
1510
|
+
return undefined;
|
|
1511
|
+
}
|
|
1512
|
+
return unit;
|
|
1513
|
+
}
|
|
1382
1514
|
function inferUnitFromRouteHints(internal) {
|
|
1383
1515
|
if (internal.routeCode) {
|
|
1384
1516
|
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",
|
|
8
|
-
{ unit: "cap",
|
|
9
|
-
{ unit: "
|
|
10
|
-
{ unit: "
|
|
11
|
-
{ unit: "
|
|
12
|
-
{ unit: "
|
|
13
|
-
{ unit: "
|
|
14
|
-
{ unit: "
|
|
15
|
-
{ unit: "
|
|
16
|
-
{ unit: "
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,154 @@ function normalizeSpacing(value) {
|
|
|
67
206
|
.trim()
|
|
68
207
|
.replace(/\s+/g, " ");
|
|
69
208
|
}
|
|
70
|
-
function
|
|
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
|
+
const PRECOMPUTED_WHEN_SEQUENCES = buildWhenSequences();
|
|
292
|
+
function tokenizeForMatching(value) {
|
|
293
|
+
return value
|
|
294
|
+
.toLowerCase()
|
|
295
|
+
.split(/\s+/)
|
|
296
|
+
.map((token) => token.replace(/^[^a-z0-9-]+|[^a-z0-9-]+$/g, ""))
|
|
297
|
+
.filter((token) => token.length > 0)
|
|
298
|
+
.filter((token) => !OPTIONAL_MATCH_TOKENS.has(token));
|
|
299
|
+
}
|
|
300
|
+
function canonicalizeForMatching(value) {
|
|
301
|
+
return tokenizeForMatching(value).join(" ");
|
|
302
|
+
}
|
|
303
|
+
function tokensMatch(prefixTokens, candidateTokens) {
|
|
304
|
+
if (prefixTokens.length === 0) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
let prefixIndex = 0;
|
|
308
|
+
for (const candidateToken of candidateTokens) {
|
|
309
|
+
if (prefixIndex >= prefixTokens.length) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
const prefixToken = prefixTokens[prefixIndex];
|
|
313
|
+
if (candidateToken.startsWith(prefixToken)) {
|
|
314
|
+
prefixIndex += 1;
|
|
315
|
+
if (prefixIndex >= prefixTokens.length) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!SKIPPABLE_CANDIDATE_TOKENS.has(candidateToken)) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return prefixIndex >= prefixTokens.length;
|
|
325
|
+
}
|
|
326
|
+
function buildUnitRoutePairs(contextUnit, options) {
|
|
327
|
+
var _a, _b;
|
|
71
328
|
const pairs = [];
|
|
72
329
|
const seen = new Set();
|
|
73
|
-
const addPair = (unit,
|
|
330
|
+
const addPair = (unit, routeOverride) => {
|
|
74
331
|
var _a;
|
|
75
|
-
|
|
332
|
+
const canonicalUnit = resolveCanonicalUnit(unit);
|
|
333
|
+
if (!canonicalUnit) {
|
|
76
334
|
return;
|
|
77
335
|
}
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
336
|
+
const normalizedUnit = normalizeKey(canonicalUnit);
|
|
337
|
+
if ((options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
|
|
338
|
+
HOUSEHOLD_VOLUME_UNIT_SET.has(normalizedUnit)) {
|
|
80
339
|
return;
|
|
81
340
|
}
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
341
|
+
const resolvedRoute = (_a = routeOverride !== null && routeOverride !== void 0 ? routeOverride : ROUTE_TOKEN_BY_UNIT.get(normalizedUnit)) !== null && _a !== void 0 ? _a : "po";
|
|
342
|
+
const cleanRoute = normalizeSpacing(resolvedRoute);
|
|
343
|
+
if (!cleanRoute) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const key = `${normalizedUnit}::${normalizeKey(cleanRoute)}`;
|
|
85
347
|
if (seen.has(key)) {
|
|
86
348
|
return;
|
|
87
349
|
}
|
|
88
350
|
seen.add(key);
|
|
89
|
-
pairs.push({ unit:
|
|
351
|
+
pairs.push({ unit: canonicalUnit, route: cleanRoute });
|
|
90
352
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (const pair of DEFAULT_UNIT_ROUTE_ORDER) {
|
|
96
|
-
addPair(pair.unit, pair.route);
|
|
353
|
+
addPair(contextUnit);
|
|
354
|
+
for (const preference of DEFAULT_UNIT_ROUTE_ORDER) {
|
|
355
|
+
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];
|
|
356
|
+
addPair(preference.unit, routeToken);
|
|
97
357
|
}
|
|
98
358
|
return pairs;
|
|
99
359
|
}
|
|
@@ -144,57 +404,99 @@ function buildDoseValues(input) {
|
|
|
144
404
|
}
|
|
145
405
|
return [...values];
|
|
146
406
|
}
|
|
147
|
-
function
|
|
407
|
+
function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher) {
|
|
148
408
|
const suggestions = [];
|
|
149
409
|
const seen = new Set();
|
|
150
410
|
const push = (value) => {
|
|
151
411
|
const normalized = normalizeSpacing(value);
|
|
152
412
|
if (!normalized) {
|
|
153
|
-
return;
|
|
413
|
+
return false;
|
|
154
414
|
}
|
|
155
415
|
const key = normalizeKey(normalized);
|
|
156
416
|
if (seen.has(key)) {
|
|
157
|
-
return;
|
|
417
|
+
return false;
|
|
158
418
|
}
|
|
159
419
|
seen.add(key);
|
|
420
|
+
if (!matcher(normalized)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
160
423
|
suggestions.push(normalized);
|
|
424
|
+
return suggestions.length >= limit;
|
|
161
425
|
};
|
|
162
426
|
for (const pair of pairs) {
|
|
427
|
+
const unitVariants = getUnitVariants(pair.unit);
|
|
163
428
|
for (const code of FREQUENCY_CODES) {
|
|
164
|
-
for (const
|
|
165
|
-
|
|
429
|
+
for (const unitVariant of unitVariants) {
|
|
430
|
+
for (const dose of doseValues) {
|
|
431
|
+
if (push(`${dose} ${unitVariant} ${pair.route} ${code}`)) {
|
|
432
|
+
return suggestions;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (push(`${pair.route} ${code}`)) {
|
|
437
|
+
return suggestions;
|
|
166
438
|
}
|
|
167
|
-
push(`${pair.route} ${code}`);
|
|
168
439
|
}
|
|
169
|
-
for (const interval of
|
|
170
|
-
for (const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
440
|
+
for (const interval of intervalTokens) {
|
|
441
|
+
for (const unitVariant of unitVariants) {
|
|
442
|
+
for (const dose of doseValues) {
|
|
443
|
+
if (push(`${dose} ${unitVariant} ${pair.route} ${interval}`)) {
|
|
444
|
+
return suggestions;
|
|
445
|
+
}
|
|
446
|
+
for (const reason of prnReasons) {
|
|
447
|
+
if (push(`${dose} ${unitVariant} ${pair.route} ${interval} prn ${reason}`)) {
|
|
448
|
+
return suggestions;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
174
451
|
}
|
|
175
452
|
}
|
|
176
|
-
push(`${pair.route} ${interval}`)
|
|
453
|
+
if (push(`${pair.route} ${interval}`)) {
|
|
454
|
+
return suggestions;
|
|
455
|
+
}
|
|
177
456
|
}
|
|
178
457
|
for (const freq of FREQUENCY_NUMBERS) {
|
|
179
458
|
const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
|
|
180
|
-
|
|
459
|
+
if (!freqToken) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (push(`1x${freq} ${pair.route} ${freqToken}`)) {
|
|
463
|
+
return suggestions;
|
|
464
|
+
}
|
|
181
465
|
for (const when of CORE_WHEN_TOKENS) {
|
|
182
|
-
push(`1x${freq} ${pair.route} ${when}`)
|
|
466
|
+
if (push(`1x${freq} ${pair.route} ${when}`)) {
|
|
467
|
+
return suggestions;
|
|
468
|
+
}
|
|
183
469
|
}
|
|
184
470
|
}
|
|
185
|
-
for (const
|
|
186
|
-
|
|
187
|
-
|
|
471
|
+
for (const whenSequence of whenSequences) {
|
|
472
|
+
const suffix = whenSequence.join(" ");
|
|
473
|
+
for (const unitVariant of unitVariants) {
|
|
474
|
+
for (const dose of doseValues) {
|
|
475
|
+
if (push(`${dose} ${unitVariant} ${pair.route} ${suffix}`)) {
|
|
476
|
+
return suggestions;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (push(`${pair.route} ${suffix}`)) {
|
|
481
|
+
return suggestions;
|
|
188
482
|
}
|
|
189
|
-
push(`${pair.route} ${when}`);
|
|
190
483
|
}
|
|
191
484
|
for (const reason of prnReasons) {
|
|
192
|
-
|
|
485
|
+
for (const unitVariant of unitVariants) {
|
|
486
|
+
for (const dose of doseValues) {
|
|
487
|
+
if (push(`${dose} ${unitVariant} ${pair.route} prn ${reason}`)) {
|
|
488
|
+
return suggestions;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (push(`${pair.route} prn ${reason}`)) {
|
|
493
|
+
return suggestions;
|
|
494
|
+
}
|
|
193
495
|
}
|
|
194
496
|
}
|
|
195
497
|
return suggestions;
|
|
196
498
|
}
|
|
197
|
-
function matchesPrefix(candidate, prefix, prefixCompact) {
|
|
499
|
+
function matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes) {
|
|
198
500
|
if (!prefix) {
|
|
199
501
|
return true;
|
|
200
502
|
}
|
|
@@ -203,26 +505,52 @@ function matchesPrefix(candidate, prefix, prefixCompact) {
|
|
|
203
505
|
return true;
|
|
204
506
|
}
|
|
205
507
|
const compactCandidate = normalizedCandidate.replace(/\s+/g, "");
|
|
206
|
-
|
|
508
|
+
if (compactCandidate.startsWith(prefixCompact)) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
const candidateNoDashes = normalizedCandidate.replace(/-/g, "");
|
|
512
|
+
if (candidateNoDashes.startsWith(prefixNoDashes)) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
const canonicalCandidate = canonicalizeForMatching(candidate);
|
|
516
|
+
if (canonicalCandidate.startsWith(prefixCanonical)) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
const canonicalCompact = canonicalCandidate.replace(/\s+/g, "");
|
|
520
|
+
if (canonicalCompact.startsWith(prefixCanonicalCompact)) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
const candidateTokens = tokenizeForMatching(candidate);
|
|
524
|
+
if (tokensMatch(prefixTokens, candidateTokens)) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
const canonicalNoDashes = canonicalCandidate.replace(/-/g, "");
|
|
528
|
+
if (canonicalNoDashes.startsWith(prefixCanonicalNoDashes)) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
const candidateTokensNoDashes = candidateTokens.map((token) => token.replace(/-/g, ""));
|
|
532
|
+
return tokensMatch(prefixTokensNoDashes, candidateTokensNoDashes);
|
|
207
533
|
}
|
|
208
534
|
function suggestSig(input, options) {
|
|
209
535
|
var _a, _b;
|
|
210
536
|
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : DEFAULT_LIMIT;
|
|
537
|
+
if (limit <= 0) {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
211
540
|
const prefix = normalizeSpacing(input.toLowerCase());
|
|
212
541
|
const prefixCompact = prefix.replace(/\s+/g, "");
|
|
542
|
+
const prefixNoDashes = prefix.replace(/-/g, "");
|
|
543
|
+
const prefixCanonical = canonicalizeForMatching(prefix);
|
|
544
|
+
const prefixCanonicalCompact = prefixCanonical.replace(/\s+/g, "");
|
|
545
|
+
const prefixCanonicalNoDashes = prefixCanonical.replace(/-/g, "");
|
|
546
|
+
const prefixTokens = tokenizeForMatching(prefixCanonical);
|
|
547
|
+
const prefixTokensNoDashes = prefixTokens.map((token) => token.replace(/-/g, ""));
|
|
213
548
|
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);
|
|
549
|
+
const pairs = buildUnitRoutePairs(contextUnit, options);
|
|
215
550
|
const doseValues = buildDoseValues(input);
|
|
216
551
|
const prnReasons = buildPrnReasons(options === null || options === void 0 ? void 0 : options.prnReasons);
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
results.push(candidate);
|
|
222
|
-
}
|
|
223
|
-
if (results.length >= limit) {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return results;
|
|
552
|
+
const intervalTokens = buildIntervalTokens(input);
|
|
553
|
+
const whenSequences = PRECOMPUTED_WHEN_SEQUENCES;
|
|
554
|
+
const matcher = (candidate) => matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes);
|
|
555
|
+
return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher);
|
|
228
556
|
}
|
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;
|