ezmedicationinput 0.1.43 → 0.1.45
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 +4 -1
- package/dist/advice.d.ts +16 -0
- package/dist/fhir-translations.d.ts +5 -0
- package/dist/fhir.d.ts +6 -4
- package/dist/format.d.ts +4 -2
- package/dist/i18n.d.ts +2 -2
- package/dist/index.cjs +18842 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +18703 -638
- package/dist/ir.d.ts +4 -0
- package/dist/lexer/lex.d.ts +2 -0
- package/dist/lexer/meaning.d.ts +71 -0
- package/dist/lexer/surface.d.ts +2 -0
- package/dist/lexer/token-types.d.ts +36 -0
- package/dist/maps.d.ts +6 -12
- package/dist/parser-state.d.ts +101 -0
- package/dist/parser.d.ts +7 -7
- package/dist/prn.d.ts +4 -0
- package/dist/site-phrases.d.ts +35 -0
- package/dist/timing-summary.d.ts +13 -3
- package/dist/types.d.ts +237 -32
- package/dist/utils/text.d.ts +3 -0
- package/package.json +17 -6
- package/dist/context.js +0 -50
- package/dist/fhir.js +0 -261
- package/dist/format.js +0 -737
- package/dist/i18n.js +0 -899
- package/dist/internal-types.d.ts +0 -60
- package/dist/internal-types.js +0 -2
- package/dist/maps.js +0 -1680
- package/dist/package.json +0 -3
- package/dist/parser.js +0 -4007
- package/dist/safety.js +0 -15
- package/dist/schedule.js +0 -1438
- package/dist/segment.js +0 -203
- package/dist/suggest.js +0 -907
- package/dist/timing-summary.js +0 -138
- package/dist/types.js +0 -228
- package/dist/utils/array.js +0 -11
- package/dist/utils/enum.d.ts +0 -2
- package/dist/utils/enum.js +0 -7
- package/dist/utils/object.js +0 -34
- package/dist/utils/strength.js +0 -149
- package/dist/utils/units.js +0 -82
package/dist/parser.js
DELETED
|
@@ -1,4007 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.tokenize = tokenize;
|
|
13
|
-
exports.findUnparsedTokenGroups = findUnparsedTokenGroups;
|
|
14
|
-
exports.parseInternal = parseInternal;
|
|
15
|
-
exports.applyPrnReasonCoding = applyPrnReasonCoding;
|
|
16
|
-
exports.applyPrnReasonCodingAsync = applyPrnReasonCodingAsync;
|
|
17
|
-
exports.applySiteCoding = applySiteCoding;
|
|
18
|
-
exports.applySiteCodingAsync = applySiteCodingAsync;
|
|
19
|
-
const maps_1 = require("./maps");
|
|
20
|
-
const context_1 = require("./context");
|
|
21
|
-
const safety_1 = require("./safety");
|
|
22
|
-
const types_1 = require("./types");
|
|
23
|
-
const object_1 = require("./utils/object");
|
|
24
|
-
const array_1 = require("./utils/array");
|
|
25
|
-
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
26
|
-
function buildCustomSiteHints(map) {
|
|
27
|
-
if (!map) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const hints = new Set();
|
|
31
|
-
const addPhraseHints = (phrase) => {
|
|
32
|
-
if (!phrase) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const normalized = (0, maps_1.normalizeBodySiteKey)(phrase);
|
|
36
|
-
if (!normalized) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
for (const part of normalized.split(" ")) {
|
|
40
|
-
if (part) {
|
|
41
|
-
hints.add(part);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
for (const [key, definition] of (0, object_1.objectEntries)(map)) {
|
|
46
|
-
addPhraseHints(key);
|
|
47
|
-
if (definition.aliases) {
|
|
48
|
-
for (const alias of definition.aliases) {
|
|
49
|
-
addPhraseHints(alias);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return hints;
|
|
54
|
-
}
|
|
55
|
-
function isBodySiteHint(word, customSiteHints) {
|
|
56
|
-
var _a;
|
|
57
|
-
return BODY_SITE_HINTS.has(word) || ((_a = customSiteHints === null || customSiteHints === void 0 ? void 0 : customSiteHints.has(word)) !== null && _a !== void 0 ? _a : false);
|
|
58
|
-
}
|
|
59
|
-
const BODY_SITE_HINTS = new Set([
|
|
60
|
-
"left",
|
|
61
|
-
"right",
|
|
62
|
-
"bilateral",
|
|
63
|
-
"arm",
|
|
64
|
-
"arms",
|
|
65
|
-
"leg",
|
|
66
|
-
"legs",
|
|
67
|
-
"thigh",
|
|
68
|
-
"thighs",
|
|
69
|
-
"shoulder",
|
|
70
|
-
"shoulders",
|
|
71
|
-
"hand",
|
|
72
|
-
"hands",
|
|
73
|
-
"foot",
|
|
74
|
-
"feet",
|
|
75
|
-
"eye",
|
|
76
|
-
"eyes",
|
|
77
|
-
"ear",
|
|
78
|
-
"ears",
|
|
79
|
-
"nostril",
|
|
80
|
-
"nostrils",
|
|
81
|
-
"abdomen",
|
|
82
|
-
"belly",
|
|
83
|
-
"cheek",
|
|
84
|
-
"cheeks",
|
|
85
|
-
"upper",
|
|
86
|
-
"lower",
|
|
87
|
-
"forearm",
|
|
88
|
-
"back",
|
|
89
|
-
"mouth",
|
|
90
|
-
"tongue",
|
|
91
|
-
"tongues",
|
|
92
|
-
"cheek",
|
|
93
|
-
"cheeks",
|
|
94
|
-
"gum",
|
|
95
|
-
"gums",
|
|
96
|
-
"tooth",
|
|
97
|
-
"teeth",
|
|
98
|
-
"nose",
|
|
99
|
-
"nares",
|
|
100
|
-
"hair",
|
|
101
|
-
"skin",
|
|
102
|
-
"scalp",
|
|
103
|
-
"face",
|
|
104
|
-
"forehead",
|
|
105
|
-
"chin",
|
|
106
|
-
"neck",
|
|
107
|
-
"buttock",
|
|
108
|
-
"buttocks",
|
|
109
|
-
"gluteal",
|
|
110
|
-
"glute",
|
|
111
|
-
"muscle",
|
|
112
|
-
"muscles",
|
|
113
|
-
"vein",
|
|
114
|
-
"veins",
|
|
115
|
-
"vagina",
|
|
116
|
-
"vaginal",
|
|
117
|
-
"penis",
|
|
118
|
-
"penile",
|
|
119
|
-
"rectum",
|
|
120
|
-
"rectal",
|
|
121
|
-
"anus",
|
|
122
|
-
"perineum",
|
|
123
|
-
"temple",
|
|
124
|
-
"temples"
|
|
125
|
-
]);
|
|
126
|
-
const SITE_CONNECTORS = new Set(["to", "in", "into", "on", "onto", "at"]);
|
|
127
|
-
const SITE_FILLER_WORDS = new Set([
|
|
128
|
-
"the",
|
|
129
|
-
"a",
|
|
130
|
-
"an",
|
|
131
|
-
"your",
|
|
132
|
-
"his",
|
|
133
|
-
"her",
|
|
134
|
-
"their",
|
|
135
|
-
"my"
|
|
136
|
-
]);
|
|
137
|
-
const HOUSEHOLD_VOLUME_UNIT_SET = new Set(maps_1.HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase()));
|
|
138
|
-
const DISCRETE_UNIT_SET = new Set([
|
|
139
|
-
"tab",
|
|
140
|
-
"tabs",
|
|
141
|
-
"tablet",
|
|
142
|
-
"tablets",
|
|
143
|
-
"cap",
|
|
144
|
-
"caps",
|
|
145
|
-
"capsule",
|
|
146
|
-
"capsules",
|
|
147
|
-
"puff",
|
|
148
|
-
"puffs",
|
|
149
|
-
"spray",
|
|
150
|
-
"sprays",
|
|
151
|
-
"drop",
|
|
152
|
-
"drops",
|
|
153
|
-
"patch",
|
|
154
|
-
"patches",
|
|
155
|
-
"suppository",
|
|
156
|
-
"suppositories",
|
|
157
|
-
"implant",
|
|
158
|
-
"implants",
|
|
159
|
-
"piece",
|
|
160
|
-
"pieces",
|
|
161
|
-
"stick",
|
|
162
|
-
"sticks",
|
|
163
|
-
"pessary",
|
|
164
|
-
"pessaries",
|
|
165
|
-
"lozenge",
|
|
166
|
-
"lozenges"
|
|
167
|
-
]);
|
|
168
|
-
const OCULAR_DIRECTION_WORDS = new Set([
|
|
169
|
-
"left",
|
|
170
|
-
"right",
|
|
171
|
-
"both",
|
|
172
|
-
"either",
|
|
173
|
-
"each",
|
|
174
|
-
"bilateral"
|
|
175
|
-
]);
|
|
176
|
-
const OCULAR_SITE_WORDS = new Set([
|
|
177
|
-
"eye",
|
|
178
|
-
"eyes",
|
|
179
|
-
"eyelid",
|
|
180
|
-
"eyelids",
|
|
181
|
-
"ocular",
|
|
182
|
-
"ophthalmic",
|
|
183
|
-
"oculus"
|
|
184
|
-
]);
|
|
185
|
-
const COMBO_EVENT_TIMINGS = {
|
|
186
|
-
"early morning": types_1.EventTiming["Early Morning"],
|
|
187
|
-
"late morning": types_1.EventTiming["Late Morning"],
|
|
188
|
-
"early afternoon": types_1.EventTiming["Early Afternoon"],
|
|
189
|
-
"late afternoon": types_1.EventTiming["Late Afternoon"],
|
|
190
|
-
"early evening": types_1.EventTiming["Early Evening"],
|
|
191
|
-
"late evening": types_1.EventTiming["Late Evening"],
|
|
192
|
-
"after sleep": types_1.EventTiming["After Sleep"],
|
|
193
|
-
"before bed": types_1.EventTiming["Before Sleep"],
|
|
194
|
-
"before bedtime": types_1.EventTiming["Before Sleep"],
|
|
195
|
-
"before sleep": types_1.EventTiming["Before Sleep"],
|
|
196
|
-
"upon waking": types_1.EventTiming.Wake
|
|
197
|
-
};
|
|
198
|
-
const DAY_RANGE_PART_PATTERN = Object.keys(maps_1.DAY_OF_WEEK_TOKENS)
|
|
199
|
-
.sort((a, b) => b.length - a.length)
|
|
200
|
-
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
201
|
-
.join("|");
|
|
202
|
-
const DAY_RANGE_SPACED_HYPHEN_REGEX = new RegExp(`(^|\\s)(${DAY_RANGE_PART_PATTERN})\\s*-\\s*(${DAY_RANGE_PART_PATTERN})(?=\\s|$)`, "giu");
|
|
203
|
-
const MEAL_CONTEXT_CONNECTORS = new Set(["and", "or", "&", "+", "plus"]);
|
|
204
|
-
const COUNT_KEYWORDS = new Set([
|
|
205
|
-
"time",
|
|
206
|
-
"times",
|
|
207
|
-
"dose",
|
|
208
|
-
"doses",
|
|
209
|
-
"application",
|
|
210
|
-
"applications",
|
|
211
|
-
"use",
|
|
212
|
-
"uses"
|
|
213
|
-
]);
|
|
214
|
-
const COUNT_CONNECTOR_WORDS = new Set([
|
|
215
|
-
"a",
|
|
216
|
-
"an",
|
|
217
|
-
"the",
|
|
218
|
-
"total",
|
|
219
|
-
"of",
|
|
220
|
-
"up",
|
|
221
|
-
"to",
|
|
222
|
-
"no",
|
|
223
|
-
"more",
|
|
224
|
-
"than",
|
|
225
|
-
"max",
|
|
226
|
-
"maximum",
|
|
227
|
-
"additional",
|
|
228
|
-
"extra"
|
|
229
|
-
]);
|
|
230
|
-
const FREQUENCY_SIMPLE_WORDS = {
|
|
231
|
-
once: 1,
|
|
232
|
-
twice: 2,
|
|
233
|
-
thrice: 3
|
|
234
|
-
};
|
|
235
|
-
const FREQUENCY_NUMBER_WORDS = {
|
|
236
|
-
one: 1,
|
|
237
|
-
two: 2,
|
|
238
|
-
three: 3,
|
|
239
|
-
four: 4,
|
|
240
|
-
five: 5,
|
|
241
|
-
six: 6,
|
|
242
|
-
seven: 7,
|
|
243
|
-
eight: 8,
|
|
244
|
-
nine: 9,
|
|
245
|
-
ten: 10,
|
|
246
|
-
eleven: 11,
|
|
247
|
-
twelve: 12
|
|
248
|
-
};
|
|
249
|
-
const FREQUENCY_TIMES_WORDS = new Set(["time", "times", "x"]);
|
|
250
|
-
const FREQUENCY_CONNECTOR_WORDS = new Set(["per", "a", "an", "each", "every"]);
|
|
251
|
-
const FREQUENCY_ADVERB_UNITS = {
|
|
252
|
-
daily: types_1.FhirPeriodUnit.Day,
|
|
253
|
-
weekly: types_1.FhirPeriodUnit.Week,
|
|
254
|
-
monthly: types_1.FhirPeriodUnit.Month,
|
|
255
|
-
hourly: types_1.FhirPeriodUnit.Hour
|
|
256
|
-
};
|
|
257
|
-
const ROUTE_DESCRIPTOR_FILLER_WORDS = new Set([
|
|
258
|
-
"per",
|
|
259
|
-
"by",
|
|
260
|
-
"via",
|
|
261
|
-
"the",
|
|
262
|
-
"a",
|
|
263
|
-
"an"
|
|
264
|
-
]);
|
|
265
|
-
function normalizeRouteDescriptorPhrase(phrase) {
|
|
266
|
-
return phrase
|
|
267
|
-
.trim()
|
|
268
|
-
.toLowerCase()
|
|
269
|
-
.split(/\s+/)
|
|
270
|
-
.filter((word) => word.length > 0 && !ROUTE_DESCRIPTOR_FILLER_WORDS.has(word))
|
|
271
|
-
.join(" ");
|
|
272
|
-
}
|
|
273
|
-
const DEFAULT_ROUTE_DESCRIPTOR_SYNONYMS = (() => {
|
|
274
|
-
const map = new Map();
|
|
275
|
-
for (const [phrase, synonym] of (0, object_1.objectEntries)(maps_1.DEFAULT_ROUTE_SYNONYMS)) {
|
|
276
|
-
const normalized = normalizeRouteDescriptorPhrase(phrase);
|
|
277
|
-
if (normalized && !map.has(normalized)) {
|
|
278
|
-
map.set(normalized, synonym);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return map;
|
|
282
|
-
})();
|
|
283
|
-
// Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
|
|
284
|
-
// logic bail early when the clinician already specified precise events.
|
|
285
|
-
const SPECIFIC_MEAL_TIMINGS = new Set([
|
|
286
|
-
types_1.EventTiming["Before Breakfast"],
|
|
287
|
-
types_1.EventTiming["Before Lunch"],
|
|
288
|
-
types_1.EventTiming["Before Dinner"],
|
|
289
|
-
types_1.EventTiming["After Breakfast"],
|
|
290
|
-
types_1.EventTiming["After Lunch"],
|
|
291
|
-
types_1.EventTiming["After Dinner"],
|
|
292
|
-
types_1.EventTiming.Breakfast,
|
|
293
|
-
types_1.EventTiming.Lunch,
|
|
294
|
-
types_1.EventTiming.Dinner
|
|
295
|
-
]);
|
|
296
|
-
// Ocular shorthand tokens commonly used in ophthalmic sigs.
|
|
297
|
-
const EYE_SITE_TOKENS = {
|
|
298
|
-
od: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
|
|
299
|
-
re: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
|
|
300
|
-
os: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
|
|
301
|
-
le: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
|
|
302
|
-
ou: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
|
|
303
|
-
be: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
|
|
304
|
-
vod: {
|
|
305
|
-
site: "right eye",
|
|
306
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
307
|
-
},
|
|
308
|
-
vos: {
|
|
309
|
-
site: "left eye",
|
|
310
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
311
|
-
},
|
|
312
|
-
ivtod: {
|
|
313
|
-
site: "right eye",
|
|
314
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
315
|
-
},
|
|
316
|
-
ivtre: {
|
|
317
|
-
site: "right eye",
|
|
318
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
319
|
-
},
|
|
320
|
-
ivtos: {
|
|
321
|
-
site: "left eye",
|
|
322
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
323
|
-
},
|
|
324
|
-
ivtle: {
|
|
325
|
-
site: "left eye",
|
|
326
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
327
|
-
},
|
|
328
|
-
ivtou: {
|
|
329
|
-
site: "both eyes",
|
|
330
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
331
|
-
},
|
|
332
|
-
ivtbe: {
|
|
333
|
-
site: "both eyes",
|
|
334
|
-
route: types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
const OPHTHALMIC_ROUTE_CODES = new Set([
|
|
338
|
-
types_1.RouteCode["Ophthalmic route"],
|
|
339
|
-
types_1.RouteCode["Ocular route (qualifier value)"],
|
|
340
|
-
types_1.RouteCode["Intravitreal route (qualifier value)"]
|
|
341
|
-
]);
|
|
342
|
-
const OPHTHALMIC_CONTEXT_TOKENS = new Set([
|
|
343
|
-
"drop",
|
|
344
|
-
"drops",
|
|
345
|
-
"gtt",
|
|
346
|
-
"gtts",
|
|
347
|
-
"eye",
|
|
348
|
-
"eyes",
|
|
349
|
-
"eyelid",
|
|
350
|
-
"eyelids",
|
|
351
|
-
"ocular",
|
|
352
|
-
"ophthalmic",
|
|
353
|
-
"ophth",
|
|
354
|
-
"oculus",
|
|
355
|
-
"os",
|
|
356
|
-
"ou",
|
|
357
|
-
"re",
|
|
358
|
-
"le",
|
|
359
|
-
"be"
|
|
360
|
-
]);
|
|
361
|
-
function normalizeTokenLower(token) {
|
|
362
|
-
return token.lower.replace(/[.{};]/g, "");
|
|
363
|
-
}
|
|
364
|
-
function hasOphthalmicContextHint(tokens, index) {
|
|
365
|
-
for (let offset = -3; offset <= 3; offset++) {
|
|
366
|
-
if (offset === 0) {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
const neighbor = tokens[index + offset];
|
|
370
|
-
if (!neighbor) {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const normalized = normalizeTokenLower(neighbor);
|
|
374
|
-
if (OPHTHALMIC_CONTEXT_TOKENS.has(normalized) || normalized.includes("eye")) {
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
function shouldInterpretOdAsOnceDaily(internal, tokens, index, treatAsSite) {
|
|
381
|
-
var _a;
|
|
382
|
-
if (treatAsSite) {
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
const hasCadenceAssigned = internal.frequency !== undefined ||
|
|
386
|
-
internal.frequencyMax !== undefined ||
|
|
387
|
-
internal.period !== undefined ||
|
|
388
|
-
internal.periodMax !== undefined ||
|
|
389
|
-
internal.timingCode !== undefined;
|
|
390
|
-
const hasPriorSiteContext = hasBodySiteContextBefore(internal, tokens, index);
|
|
391
|
-
const hasUpcomingSiteContext = hasBodySiteContextAfter(internal, tokens, index);
|
|
392
|
-
const previous = tokens[index - 1];
|
|
393
|
-
const previousNormalized = previous ? normalizeTokenLower(previous) : undefined;
|
|
394
|
-
const previousIsOd = previousNormalized === "od";
|
|
395
|
-
const previousConsumed = previousIsOd && internal.consumed.has(previous.index);
|
|
396
|
-
const previousOdProvidedSite = previousConsumed && /eye/i.test((_a = internal.siteText) !== null && _a !== void 0 ? _a : "");
|
|
397
|
-
if (previousOdProvidedSite) {
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
const previousEyeToken = previousNormalized && previousNormalized !== "od"
|
|
401
|
-
? EYE_SITE_TOKENS[previousNormalized]
|
|
402
|
-
: undefined;
|
|
403
|
-
if (previousEyeToken && internal.consumed.has(previous.index)) {
|
|
404
|
-
return true;
|
|
405
|
-
}
|
|
406
|
-
if (previousNormalized === "od" &&
|
|
407
|
-
internal.siteSource === "abbreviation" &&
|
|
408
|
-
internal.siteText &&
|
|
409
|
-
/eye/i.test(internal.siteText)) {
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
if (hasPriorSiteContext || hasUpcomingSiteContext) {
|
|
413
|
-
return !hasCadenceAssigned;
|
|
414
|
-
}
|
|
415
|
-
if (hasCadenceAssigned) {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
|
|
419
|
-
return true;
|
|
420
|
-
}
|
|
421
|
-
if (internal.unit && internal.unit !== "drop") {
|
|
422
|
-
return true;
|
|
423
|
-
}
|
|
424
|
-
if (internal.siteText && !/eye/i.test(internal.siteText)) {
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
const hasNonOdToken = tokens.some((token, tokenIndex) => {
|
|
428
|
-
if (tokenIndex === index) {
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
|
-
return normalizeTokenLower(token) !== "od";
|
|
432
|
-
});
|
|
433
|
-
if (!hasNonOdToken) {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
|
|
437
|
-
(internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
|
|
438
|
-
(internal.siteText !== undefined && /eye/i.test(internal.siteText));
|
|
439
|
-
if (ophthalmicContext && hasSpelledOcularSiteBefore(tokens, index)) {
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
return !ophthalmicContext;
|
|
443
|
-
}
|
|
444
|
-
function hasBodySiteContextBefore(internal, tokens, index) {
|
|
445
|
-
const currentToken = tokens[index];
|
|
446
|
-
const currentTokenIndex = currentToken ? currentToken.index : index;
|
|
447
|
-
if (internal.siteText) {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
for (const tokenIndex of internal.siteTokenIndices) {
|
|
451
|
-
if (tokenIndex < currentTokenIndex) {
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
for (let i = 0; i < index; i++) {
|
|
456
|
-
const token = tokens[i];
|
|
457
|
-
if (!token) {
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (internal.consumed.has(token.index)) {
|
|
461
|
-
if (internal.siteTokenIndices.has(token.index) && token.index < currentTokenIndex) {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
const normalized = normalizeTokenLower(token);
|
|
467
|
-
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
468
|
-
return true;
|
|
469
|
-
}
|
|
470
|
-
if (EYE_SITE_TOKENS[normalized]) {
|
|
471
|
-
return true;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
return false;
|
|
475
|
-
}
|
|
476
|
-
function hasBodySiteContextAfter(internal, tokens, index) {
|
|
477
|
-
const currentToken = tokens[index];
|
|
478
|
-
const currentTokenIndex = currentToken ? currentToken.index : index;
|
|
479
|
-
for (const tokenIndex of internal.siteTokenIndices) {
|
|
480
|
-
if (tokenIndex > currentTokenIndex) {
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
let seenConnector = false;
|
|
485
|
-
for (let i = index + 1; i < tokens.length; i++) {
|
|
486
|
-
const token = tokens[i];
|
|
487
|
-
if (!token) {
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
if (internal.consumed.has(token.index)) {
|
|
491
|
-
if (internal.siteTokenIndices.has(token.index) && token.index > currentTokenIndex) {
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
const normalized = normalizeTokenLower(token);
|
|
497
|
-
if (SITE_CONNECTORS.has(normalized)) {
|
|
498
|
-
seenConnector = true;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
if (SITE_FILLER_WORDS.has(normalized)) {
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
if (seenConnector) {
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
if (!seenConnector) {
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
function hasSpelledOcularSiteBefore(tokens, index) {
|
|
517
|
-
let hasOcularWord = false;
|
|
518
|
-
let hasDirectionalCue = false;
|
|
519
|
-
for (let i = 0; i < index; i++) {
|
|
520
|
-
const token = tokens[i];
|
|
521
|
-
if (!token) {
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
const normalized = normalizeTokenLower(token);
|
|
525
|
-
if (SITE_CONNECTORS.has(normalized) || OCULAR_DIRECTION_WORDS.has(normalized)) {
|
|
526
|
-
hasDirectionalCue = true;
|
|
527
|
-
}
|
|
528
|
-
if (OCULAR_SITE_WORDS.has(normalized) || normalized.includes("eye")) {
|
|
529
|
-
hasOcularWord = true;
|
|
530
|
-
}
|
|
531
|
-
if (hasDirectionalCue && hasOcularWord) {
|
|
532
|
-
return true;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return false;
|
|
536
|
-
}
|
|
537
|
-
function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
|
|
538
|
-
var _a;
|
|
539
|
-
const currentToken = tokens[index];
|
|
540
|
-
const normalizedSelf = normalizeTokenLower(currentToken);
|
|
541
|
-
const eyeMeta = EYE_SITE_TOKENS[normalizedSelf];
|
|
542
|
-
const contextRoute = (0, context_1.inferRouteFromContext)(context !== null && context !== void 0 ? context : undefined);
|
|
543
|
-
if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
546
|
-
if (contextRoute && !OPHTHALMIC_ROUTE_CODES.has(contextRoute)) {
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
if (internal.siteText) {
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
if (internal.siteSource === "abbreviation") {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
const dosageForm = (_a = context === null || context === void 0 ? void 0 : context.dosageForm) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
556
|
-
const contextImpliesOphthalmic = contextRoute
|
|
557
|
-
? OPHTHALMIC_ROUTE_CODES.has(contextRoute)
|
|
558
|
-
: Boolean(dosageForm && /(eye|ophth|ocular|intravit)/i.test(dosageForm));
|
|
559
|
-
const eyeRouteImpliesOphthalmic = (eyeMeta === null || eyeMeta === void 0 ? void 0 : eyeMeta.route) === types_1.RouteCode["Intravitreal route (qualifier value)"];
|
|
560
|
-
const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
|
|
561
|
-
(internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
|
|
562
|
-
contextImpliesOphthalmic ||
|
|
563
|
-
eyeRouteImpliesOphthalmic;
|
|
564
|
-
if (hasBodySiteContextAfter(internal, tokens, index)) {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
if (!ophthalmicContext) {
|
|
568
|
-
const hasOtherActiveTokens = tokens.some((token, tokenIndex) => tokenIndex !== index && !internal.consumed.has(token.index));
|
|
569
|
-
const onlyEyeTokens = tokens.every((token, tokenIndex) => {
|
|
570
|
-
if (tokenIndex === index || internal.consumed.has(token.index)) {
|
|
571
|
-
return true;
|
|
572
|
-
}
|
|
573
|
-
return normalizeTokenLower(token) === "od";
|
|
574
|
-
});
|
|
575
|
-
if (!hasOtherActiveTokens) {
|
|
576
|
-
return internal.unit === undefined && internal.routeCode === undefined;
|
|
577
|
-
}
|
|
578
|
-
if (onlyEyeTokens) {
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
return false;
|
|
582
|
-
}
|
|
583
|
-
for (let i = 0; i < index; i++) {
|
|
584
|
-
const candidate = tokens[i];
|
|
585
|
-
if (internal.consumed.has(candidate.index)) {
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
const normalized = normalizeTokenLower(candidate);
|
|
589
|
-
if (SITE_CONNECTORS.has(normalized)) {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
if (EYE_SITE_TOKENS[normalized]) {
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
if (maps_1.DEFAULT_ROUTE_SYNONYMS[normalized]) {
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return true;
|
|
603
|
-
}
|
|
604
|
-
function tryParseNumericCadence(internal, tokens, index) {
|
|
605
|
-
const token = tokens[index];
|
|
606
|
-
if (!/^[0-9]+(?:\.[0-9]+)?$/.test(token.lower)) {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
if (internal.frequency !== undefined ||
|
|
610
|
-
internal.frequencyMax !== undefined ||
|
|
611
|
-
internal.period !== undefined ||
|
|
612
|
-
internal.periodMax !== undefined) {
|
|
613
|
-
return false;
|
|
614
|
-
}
|
|
615
|
-
let nextIndex = index + 1;
|
|
616
|
-
const connectors = [];
|
|
617
|
-
while (true) {
|
|
618
|
-
const connector = tokens[nextIndex];
|
|
619
|
-
if (!connector || internal.consumed.has(connector.index)) {
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
const normalized = normalizeTokenLower(connector);
|
|
623
|
-
if (normalized === "per" || normalized === "a" || normalized === "each" || normalized === "every") {
|
|
624
|
-
connectors.push(connector);
|
|
625
|
-
nextIndex += 1;
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
if (!connectors.length) {
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
const unitToken = tokens[nextIndex];
|
|
634
|
-
if (!unitToken || internal.consumed.has(unitToken.index)) {
|
|
635
|
-
return false;
|
|
636
|
-
}
|
|
637
|
-
const unitCode = mapIntervalUnit(normalizeTokenLower(unitToken));
|
|
638
|
-
if (!unitCode) {
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
|
-
const value = parseFloat(token.original);
|
|
642
|
-
if (!Number.isFinite(value)) {
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
internal.frequency = value;
|
|
646
|
-
internal.period = 1;
|
|
647
|
-
internal.periodUnit = unitCode;
|
|
648
|
-
if (value === 1 && unitCode === types_1.FhirPeriodUnit.Day && !internal.timingCode) {
|
|
649
|
-
internal.timingCode = "QD";
|
|
650
|
-
}
|
|
651
|
-
mark(internal.consumed, token);
|
|
652
|
-
for (const connector of connectors) {
|
|
653
|
-
mark(internal.consumed, connector);
|
|
654
|
-
}
|
|
655
|
-
mark(internal.consumed, unitToken);
|
|
656
|
-
return true;
|
|
657
|
-
}
|
|
658
|
-
function tryParseCountBasedFrequency(internal, tokens, index, options) {
|
|
659
|
-
const token = tokens[index];
|
|
660
|
-
if (internal.consumed.has(token.index)) {
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
663
|
-
if (internal.frequency !== undefined ||
|
|
664
|
-
internal.frequencyMax !== undefined ||
|
|
665
|
-
internal.period !== undefined ||
|
|
666
|
-
internal.periodMax !== undefined) {
|
|
667
|
-
return false;
|
|
668
|
-
}
|
|
669
|
-
const normalized = normalizeTokenLower(token);
|
|
670
|
-
let value;
|
|
671
|
-
let requiresPeriod = true;
|
|
672
|
-
let requiresCue = true;
|
|
673
|
-
if (/^[0-9]+(?:\.[0-9]+)?$/.test(normalized)) {
|
|
674
|
-
value = parseFloat(token.original);
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
const simple = FREQUENCY_SIMPLE_WORDS[normalized];
|
|
678
|
-
if (simple !== undefined) {
|
|
679
|
-
value = simple;
|
|
680
|
-
requiresPeriod = false;
|
|
681
|
-
requiresCue = false;
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
const wordValue = FREQUENCY_NUMBER_WORDS[normalized];
|
|
685
|
-
if (wordValue === undefined) {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
|
-
value = wordValue;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
if (!Number.isFinite(value) || value === undefined || value <= 0) {
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
const nextToken = tokens[index + 1];
|
|
695
|
-
if (nextToken &&
|
|
696
|
-
!internal.consumed.has(nextToken.index) &&
|
|
697
|
-
normalizeUnit(normalizeTokenLower(nextToken), options)) {
|
|
698
|
-
return false;
|
|
699
|
-
}
|
|
700
|
-
const partsToConsume = [];
|
|
701
|
-
let nextIndex = index + 1;
|
|
702
|
-
let periodUnit;
|
|
703
|
-
let sawCue = !requiresCue;
|
|
704
|
-
let sawTimesWord = false;
|
|
705
|
-
let sawConnectorWord = false;
|
|
706
|
-
while (true) {
|
|
707
|
-
const candidate = tokens[nextIndex];
|
|
708
|
-
if (!candidate || internal.consumed.has(candidate.index)) {
|
|
709
|
-
break;
|
|
710
|
-
}
|
|
711
|
-
const lower = normalizeTokenLower(candidate);
|
|
712
|
-
if (FREQUENCY_TIMES_WORDS.has(lower)) {
|
|
713
|
-
partsToConsume.push(candidate);
|
|
714
|
-
sawCue = true;
|
|
715
|
-
sawTimesWord = true;
|
|
716
|
-
nextIndex += 1;
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
if (FREQUENCY_CONNECTOR_WORDS.has(lower)) {
|
|
720
|
-
partsToConsume.push(candidate);
|
|
721
|
-
sawCue = true;
|
|
722
|
-
sawConnectorWord = true;
|
|
723
|
-
nextIndex += 1;
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
const adverbUnit = mapFrequencyAdverb(lower);
|
|
727
|
-
if (adverbUnit) {
|
|
728
|
-
periodUnit = adverbUnit;
|
|
729
|
-
partsToConsume.push(candidate);
|
|
730
|
-
break;
|
|
731
|
-
}
|
|
732
|
-
const mappedUnit = mapIntervalUnit(lower);
|
|
733
|
-
if (mappedUnit) {
|
|
734
|
-
periodUnit = mappedUnit;
|
|
735
|
-
partsToConsume.push(candidate);
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
if (!periodUnit) {
|
|
741
|
-
if (requiresPeriod) {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
periodUnit = types_1.FhirPeriodUnit.Day;
|
|
745
|
-
}
|
|
746
|
-
if (requiresCue && !sawCue) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
internal.frequency = value;
|
|
750
|
-
internal.period = 1;
|
|
751
|
-
internal.periodUnit = periodUnit;
|
|
752
|
-
if (value === 1 && periodUnit === types_1.FhirPeriodUnit.Day && !internal.timingCode) {
|
|
753
|
-
internal.timingCode = "QD";
|
|
754
|
-
}
|
|
755
|
-
let consumeCurrentToken = true;
|
|
756
|
-
if (value === 1 && !sawConnectorWord && sawTimesWord && periodUnit !== types_1.FhirPeriodUnit.Day) {
|
|
757
|
-
consumeCurrentToken = false;
|
|
758
|
-
}
|
|
759
|
-
if (consumeCurrentToken) {
|
|
760
|
-
mark(internal.consumed, token);
|
|
761
|
-
}
|
|
762
|
-
for (const part of partsToConsume) {
|
|
763
|
-
mark(internal.consumed, part);
|
|
764
|
-
}
|
|
765
|
-
return consumeCurrentToken;
|
|
766
|
-
}
|
|
767
|
-
function parseTimeToFhir(timeStr) {
|
|
768
|
-
const clean = timeStr.toLowerCase().trim();
|
|
769
|
-
// Match 9:00, 9.00, 9:00am, 9pm, 9 am, 9
|
|
770
|
-
const match = clean.match(/^(\d{1,2})[:.](\d{2})\s*(am|pm)?$/) ||
|
|
771
|
-
clean.match(/^(\d{1,2})\s*(am|pm)$/) ||
|
|
772
|
-
clean.match(/^(\d{1,2})$/);
|
|
773
|
-
if (!match)
|
|
774
|
-
return undefined;
|
|
775
|
-
let hour = parseInt(match[1], 10);
|
|
776
|
-
let minute = 0;
|
|
777
|
-
let ampm;
|
|
778
|
-
if (match[2] && !isNaN(parseInt(match[2], 10))) {
|
|
779
|
-
minute = parseInt(match[2], 10);
|
|
780
|
-
ampm = match[3];
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
ampm = match[2];
|
|
784
|
-
}
|
|
785
|
-
if (ampm === "pm" && hour < 12)
|
|
786
|
-
hour += 12;
|
|
787
|
-
if (ampm === "am" && hour === 12)
|
|
788
|
-
hour = 0;
|
|
789
|
-
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
790
|
-
return undefined;
|
|
791
|
-
const h = hour < 10 ? `0${hour}` : `${hour}`;
|
|
792
|
-
const m = minute < 10 ? `0${minute}` : `${minute}`;
|
|
793
|
-
return `${h}:${m}:00`;
|
|
794
|
-
}
|
|
795
|
-
function extractAttachedAtTimeToken(lower) {
|
|
796
|
-
if (lower.length <= 1) {
|
|
797
|
-
return undefined;
|
|
798
|
-
}
|
|
799
|
-
if (lower.charAt(0) === "@") {
|
|
800
|
-
const candidate = lower.slice(1);
|
|
801
|
-
return parseTimeToFhir(candidate) ? candidate : undefined;
|
|
802
|
-
}
|
|
803
|
-
if (lower.startsWith("at") && lower.length > 2 && /^\d/.test(lower.charAt(2))) {
|
|
804
|
-
const candidate = lower.slice(2);
|
|
805
|
-
return parseTimeToFhir(candidate) ? candidate : undefined;
|
|
806
|
-
}
|
|
807
|
-
return undefined;
|
|
808
|
-
}
|
|
809
|
-
function isAtPrefixToken(lower) {
|
|
810
|
-
return lower === "@" || lower === "at" || extractAttachedAtTimeToken(lower) !== undefined;
|
|
811
|
-
}
|
|
812
|
-
function tryParseTimeBasedSchedule(internal, tokens, index) {
|
|
813
|
-
const token = tokens[index];
|
|
814
|
-
if (internal.consumed.has(token.index))
|
|
815
|
-
return false;
|
|
816
|
-
const attachedAtTime = extractAttachedAtTimeToken(token.lower);
|
|
817
|
-
const isAtPrefix = isAtPrefixToken(token.lower);
|
|
818
|
-
if (!isAtPrefix && !/^\d/.test(token.lower))
|
|
819
|
-
return false;
|
|
820
|
-
let nextIndex = index;
|
|
821
|
-
const times = [];
|
|
822
|
-
const consumedIndices = [];
|
|
823
|
-
const timeTokens = [];
|
|
824
|
-
if (token.lower === "@" || token.lower === "at") {
|
|
825
|
-
consumedIndices.push(index);
|
|
826
|
-
nextIndex++;
|
|
827
|
-
}
|
|
828
|
-
else if (attachedAtTime) {
|
|
829
|
-
let timeStr = attachedAtTime;
|
|
830
|
-
const lookaheadIndices = [];
|
|
831
|
-
if (!timeStr.includes("am") && !timeStr.includes("pm")) {
|
|
832
|
-
const ampmToken = tokens[index + 1];
|
|
833
|
-
if (ampmToken &&
|
|
834
|
-
!internal.consumed.has(ampmToken.index) &&
|
|
835
|
-
(ampmToken.lower === "am" || ampmToken.lower === "pm")) {
|
|
836
|
-
timeStr += ampmToken.lower;
|
|
837
|
-
lookaheadIndices.push(index + 1);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const compactTime = parseTimeToFhir(timeStr);
|
|
841
|
-
if (!compactTime) {
|
|
842
|
-
return false;
|
|
843
|
-
}
|
|
844
|
-
times.push(compactTime);
|
|
845
|
-
timeTokens.push(timeStr);
|
|
846
|
-
consumedIndices.push(index);
|
|
847
|
-
for (const idx of lookaheadIndices) {
|
|
848
|
-
consumedIndices.push(idx);
|
|
849
|
-
}
|
|
850
|
-
nextIndex = index + 1 + lookaheadIndices.length;
|
|
851
|
-
}
|
|
852
|
-
while (nextIndex < tokens.length) {
|
|
853
|
-
const nextToken = tokens[nextIndex];
|
|
854
|
-
if (!nextToken || internal.consumed.has(nextToken.index))
|
|
855
|
-
break;
|
|
856
|
-
if ((nextToken.lower === "," || nextToken.lower === "and") && times.length > 0) {
|
|
857
|
-
const peekToken = tokens[nextIndex + 1];
|
|
858
|
-
if (peekToken && !internal.consumed.has(peekToken.index)) {
|
|
859
|
-
let peekStr = peekToken.lower;
|
|
860
|
-
const ampmToken = tokens[nextIndex + 2];
|
|
861
|
-
if (ampmToken &&
|
|
862
|
-
!internal.consumed.has(ampmToken.index) &&
|
|
863
|
-
(ampmToken.lower === "am" || ampmToken.lower === "pm")) {
|
|
864
|
-
peekStr += ampmToken.lower;
|
|
865
|
-
}
|
|
866
|
-
if (parseTimeToFhir(peekStr)) {
|
|
867
|
-
consumedIndices.push(nextIndex);
|
|
868
|
-
nextIndex++;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
let timeStr = nextToken.lower;
|
|
874
|
-
let lookaheadIndices = [];
|
|
875
|
-
// Look ahead for am/pm if current token is just a number or doesn't have am/pm
|
|
876
|
-
if (!timeStr.includes("am") && !timeStr.includes("pm")) {
|
|
877
|
-
const nextNext = tokens[nextIndex + 1];
|
|
878
|
-
if (nextNext && !internal.consumed.has(nextNext.index) && (nextNext.lower === "am" || nextNext.lower === "pm")) {
|
|
879
|
-
timeStr += nextNext.lower;
|
|
880
|
-
lookaheadIndices.push(nextIndex + 1);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
const time = parseTimeToFhir(timeStr);
|
|
884
|
-
if (time) {
|
|
885
|
-
times.push(time);
|
|
886
|
-
timeTokens.push(timeStr);
|
|
887
|
-
consumedIndices.push(nextIndex);
|
|
888
|
-
for (const idx of lookaheadIndices) {
|
|
889
|
-
consumedIndices.push(idx);
|
|
890
|
-
}
|
|
891
|
-
nextIndex += 1 + lookaheadIndices.length;
|
|
892
|
-
// Support comma or space separated times
|
|
893
|
-
const separatorToken = tokens[nextIndex];
|
|
894
|
-
// Check if there is another time after the separator
|
|
895
|
-
if (separatorToken && (separatorToken.lower === "," || separatorToken.lower === "and")) {
|
|
896
|
-
// Peek for next time
|
|
897
|
-
let peekIndex = nextIndex + 1;
|
|
898
|
-
let peekToken = tokens[peekIndex];
|
|
899
|
-
if (peekToken) {
|
|
900
|
-
let peekStr = peekToken.lower;
|
|
901
|
-
let peekNext = tokens[peekIndex + 1];
|
|
902
|
-
if (peekNext && !internal.consumed.has(peekNext.index) && (peekNext.lower === "am" || peekNext.lower === "pm")) {
|
|
903
|
-
peekStr += peekNext.lower;
|
|
904
|
-
}
|
|
905
|
-
if (parseTimeToFhir(peekStr)) {
|
|
906
|
-
consumedIndices.push(nextIndex);
|
|
907
|
-
nextIndex++;
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
if (times.length > 0) {
|
|
917
|
-
if (!isAtPrefix) {
|
|
918
|
-
const hasClearTimeFormat = timeTokens.some((t) => t.includes(":") || t.includes("am") || t.includes("pm"));
|
|
919
|
-
if (!hasClearTimeFormat) {
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
internal.timeOfDay = internal.timeOfDay || [];
|
|
924
|
-
for (const time of times) {
|
|
925
|
-
if (!(0, array_1.arrayIncludes)(internal.timeOfDay, time)) {
|
|
926
|
-
internal.timeOfDay.push(time);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
for (const idx of consumedIndices) {
|
|
930
|
-
mark(internal.consumed, tokens[idx]);
|
|
931
|
-
}
|
|
932
|
-
return true;
|
|
933
|
-
}
|
|
934
|
-
return false;
|
|
935
|
-
}
|
|
936
|
-
const SITE_UNIT_ROUTE_HINTS = [
|
|
937
|
-
{ pattern: /\beye(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
|
|
938
|
-
{ pattern: /\beyelid(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
|
|
939
|
-
{ pattern: /\bintravitreal\b/i, route: types_1.RouteCode["Intravitreal route (qualifier value)"] },
|
|
940
|
-
{ pattern: /\bear(s)?\b/i, route: types_1.RouteCode["Otic route"] },
|
|
941
|
-
{ pattern: /\bnostril(s)?\b/i, route: types_1.RouteCode["Nasal route"] },
|
|
942
|
-
{ pattern: /\bnares?\b/i, route: types_1.RouteCode["Nasal route"] },
|
|
943
|
-
{ pattern: /\bnose\b/i, route: types_1.RouteCode["Nasal route"] },
|
|
944
|
-
{ pattern: /\bmouth\b/i, route: types_1.RouteCode["Oral route"] },
|
|
945
|
-
{ pattern: /\boral\b/i, route: types_1.RouteCode["Oral route"] },
|
|
946
|
-
{ pattern: /\bunder (the )?tongue\b/i, route: types_1.RouteCode["Sublingual route"] },
|
|
947
|
-
{ pattern: /\btongue\b/i, route: types_1.RouteCode["Sublingual route"] },
|
|
948
|
-
{ pattern: /\bcheek(s)?\b/i, route: types_1.RouteCode["Buccal route"] },
|
|
949
|
-
{ pattern: /\blung(s)?\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
|
|
950
|
-
{ pattern: /\brespiratory tract\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
|
|
951
|
-
{ pattern: /\bskin\b/i, route: types_1.RouteCode["Topical route"] },
|
|
952
|
-
{ pattern: /\bscalp\b/i, route: types_1.RouteCode["Topical route"] },
|
|
953
|
-
{ pattern: /\bface\b/i, route: types_1.RouteCode["Topical route"] },
|
|
954
|
-
{ pattern: /\bhand(s)?\b/i, route: types_1.RouteCode["Topical route"] },
|
|
955
|
-
{ pattern: /(\bfoot\b|\bfeet\b)/i, route: types_1.RouteCode["Topical route"] },
|
|
956
|
-
{ pattern: /\belbow(s)?\b/i, route: types_1.RouteCode["Topical route"] },
|
|
957
|
-
{ pattern: /\bknee(s)?\b/i, route: types_1.RouteCode["Topical route"] },
|
|
958
|
-
{ pattern: /\bleg(s)?\b/i, route: types_1.RouteCode["Topical route"] },
|
|
959
|
-
{ pattern: /\barm(s)?\b/i, route: types_1.RouteCode["Topical route"] },
|
|
960
|
-
{ pattern: /\bpatch(es)?\b/i, route: types_1.RouteCode["Transdermal route"] },
|
|
961
|
-
{ pattern: /\babdomen\b/i, route: types_1.RouteCode["Subcutaneous route"] },
|
|
962
|
-
{ pattern: /\bbelly\b/i, route: types_1.RouteCode["Subcutaneous route"] },
|
|
963
|
-
{ pattern: /\bstomach\b/i, route: types_1.RouteCode["Subcutaneous route"] },
|
|
964
|
-
{ pattern: /\bthigh(s)?\b/i, route: types_1.RouteCode["Subcutaneous route"] },
|
|
965
|
-
{ pattern: /\bupper arm\b/i, route: types_1.RouteCode["Subcutaneous route"] },
|
|
966
|
-
{ pattern: /\bbuttock(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
|
|
967
|
-
{ pattern: /\bglute(al)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
|
|
968
|
-
{ pattern: /\bdeltoid\b/i, route: types_1.RouteCode["Intramuscular route"] },
|
|
969
|
-
{ pattern: /\bmuscle(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
|
|
970
|
-
{ pattern: /\bvein(s)?\b/i, route: types_1.RouteCode["Intravenous route"] },
|
|
971
|
-
{ pattern: /\brectum\b/i, route: types_1.RouteCode["Per rectum"] },
|
|
972
|
-
{ pattern: /\banus\b/i, route: types_1.RouteCode["Per rectum"] },
|
|
973
|
-
{ pattern: /\brectal\b/i, route: types_1.RouteCode["Per rectum"] },
|
|
974
|
-
{ pattern: /\bvagina\b/i, route: types_1.RouteCode["Per vagina"] },
|
|
975
|
-
{ pattern: /\bvaginal\b/i, route: types_1.RouteCode["Per vagina"] }
|
|
976
|
-
];
|
|
977
|
-
function tokenize(input) {
|
|
978
|
-
const separators = /[(),;]/g;
|
|
979
|
-
let normalized = input.trim().replace(separators, " ");
|
|
980
|
-
normalized = normalized.replace(DAY_RANGE_SPACED_HYPHEN_REGEX, (_match, prefix, start, end) => `${prefix}${start}-${end}`);
|
|
981
|
-
normalized = normalized.replace(/\s-\s/g, " ; ");
|
|
982
|
-
normalized = normalized.replace(/(\d+(?:\.\d+)?)\s*\/\s*(d|day|days|wk|w|week|weeks|mo|month|months|hr|hrs|hour|hours|h|min|mins|minute|minutes)\b/gi, (_match, value, unit) => `${value} per ${unit}`);
|
|
983
|
-
normalized = normalized.replace(/(\d+)\s*\/\s*(\d+)/g, (match, num, den) => {
|
|
984
|
-
const numerator = parseFloat(num);
|
|
985
|
-
const denominator = parseFloat(den);
|
|
986
|
-
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) {
|
|
987
|
-
return match;
|
|
988
|
-
}
|
|
989
|
-
const value = numerator / denominator;
|
|
990
|
-
return value.toString();
|
|
991
|
-
});
|
|
992
|
-
normalized = normalized.replace(/(\d+(?:\.\d+)?[x*])([A-Za-z]+)/g, "$1 $2");
|
|
993
|
-
normalized = normalized.replace(/(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)/g, "$1-$2");
|
|
994
|
-
normalized = normalized.replace(/(\d+(?:\.\d+)?)(tab|tabs|tablet|tablets|cap|caps|capsule|capsules|mg|mcg|ml|g|drops|drop|puff|puffs|spray|sprays|patch|patches)/gi, "$1 $2");
|
|
995
|
-
normalized = normalized.replace(/[\\/]/g, " ");
|
|
996
|
-
const rawTokens = normalized
|
|
997
|
-
.split(/\s+/)
|
|
998
|
-
.map((t) => t.trim())
|
|
999
|
-
.filter((t) => t.length > 0 && t !== "." && t !== "-");
|
|
1000
|
-
const tokens = [];
|
|
1001
|
-
for (let i = 0; i < rawTokens.length; i++) {
|
|
1002
|
-
const raw = rawTokens[i];
|
|
1003
|
-
const parts = splitToken(raw);
|
|
1004
|
-
for (const part of parts) {
|
|
1005
|
-
if (!part)
|
|
1006
|
-
continue;
|
|
1007
|
-
tokens.push({ original: part, lower: part.toLowerCase(), index: tokens.length });
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return tokens;
|
|
1011
|
-
}
|
|
1012
|
-
/**
|
|
1013
|
-
* Locates the span of the detected site tokens within the caller's original
|
|
1014
|
-
* input so downstream consumers can highlight or replace the exact substring.
|
|
1015
|
-
*/
|
|
1016
|
-
function computeTokenRange(input, tokens, indices) {
|
|
1017
|
-
if (!indices.length) {
|
|
1018
|
-
return undefined;
|
|
1019
|
-
}
|
|
1020
|
-
const lowerInput = input.toLowerCase();
|
|
1021
|
-
let searchStart = 0;
|
|
1022
|
-
let rangeStart;
|
|
1023
|
-
let rangeEnd;
|
|
1024
|
-
for (const tokenIndex of indices) {
|
|
1025
|
-
const token = tokens[tokenIndex];
|
|
1026
|
-
if (!token) {
|
|
1027
|
-
continue;
|
|
1028
|
-
}
|
|
1029
|
-
const segment = token.original.trim();
|
|
1030
|
-
if (!segment) {
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
const lowerSegment = segment.toLowerCase();
|
|
1034
|
-
const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
|
|
1035
|
-
if (foundIndex === -1) {
|
|
1036
|
-
return undefined;
|
|
1037
|
-
}
|
|
1038
|
-
const segmentEnd = foundIndex + lowerSegment.length;
|
|
1039
|
-
if (rangeStart === undefined) {
|
|
1040
|
-
rangeStart = foundIndex;
|
|
1041
|
-
}
|
|
1042
|
-
rangeEnd = segmentEnd;
|
|
1043
|
-
searchStart = segmentEnd;
|
|
1044
|
-
}
|
|
1045
|
-
if (rangeStart === undefined || rangeEnd === undefined) {
|
|
1046
|
-
return undefined;
|
|
1047
|
-
}
|
|
1048
|
-
return { start: rangeStart, end: rangeEnd };
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* Prefers highlighting the sanitized site text when it can be located directly
|
|
1052
|
-
* in the original input; otherwise falls back to the broader token-derived
|
|
1053
|
-
* range.
|
|
1054
|
-
*/
|
|
1055
|
-
function refineSiteRange(input, sanitized, tokenRange) {
|
|
1056
|
-
if (!input) {
|
|
1057
|
-
return tokenRange;
|
|
1058
|
-
}
|
|
1059
|
-
const trimmed = sanitized.trim();
|
|
1060
|
-
if (!trimmed) {
|
|
1061
|
-
return tokenRange;
|
|
1062
|
-
}
|
|
1063
|
-
const lowerInput = input.toLowerCase();
|
|
1064
|
-
const lowerSanitized = trimmed.toLowerCase();
|
|
1065
|
-
let startIndex = tokenRange ? lowerInput.indexOf(lowerSanitized, tokenRange.start) : -1;
|
|
1066
|
-
if (startIndex === -1) {
|
|
1067
|
-
startIndex = lowerInput.indexOf(lowerSanitized);
|
|
1068
|
-
}
|
|
1069
|
-
if (startIndex === -1) {
|
|
1070
|
-
return tokenRange;
|
|
1071
|
-
}
|
|
1072
|
-
return { start: startIndex, end: startIndex + lowerSanitized.length };
|
|
1073
|
-
}
|
|
1074
|
-
function findUnparsedTokenGroups(internal) {
|
|
1075
|
-
const leftoverTokens = internal.tokens
|
|
1076
|
-
.filter((token) => !internal.consumed.has(token.index))
|
|
1077
|
-
.sort((a, b) => a.index - b.index);
|
|
1078
|
-
if (leftoverTokens.length === 0) {
|
|
1079
|
-
return [];
|
|
1080
|
-
}
|
|
1081
|
-
const groups = [];
|
|
1082
|
-
let currentGroup = [];
|
|
1083
|
-
let previousIndex;
|
|
1084
|
-
let minimumStart = 0;
|
|
1085
|
-
const locateRange = (tokensToLocate, initial) => {
|
|
1086
|
-
const lowerInput = internal.input.toLowerCase();
|
|
1087
|
-
let searchStart = minimumStart;
|
|
1088
|
-
let rangeStart;
|
|
1089
|
-
let rangeEnd;
|
|
1090
|
-
for (const token of tokensToLocate) {
|
|
1091
|
-
const segment = token.original.trim();
|
|
1092
|
-
if (!segment) {
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
const lowerSegment = segment.toLowerCase();
|
|
1096
|
-
const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
|
|
1097
|
-
if (foundIndex === -1) {
|
|
1098
|
-
return initial;
|
|
1099
|
-
}
|
|
1100
|
-
if (rangeStart === undefined) {
|
|
1101
|
-
rangeStart = foundIndex;
|
|
1102
|
-
}
|
|
1103
|
-
const segmentEnd = foundIndex + lowerSegment.length;
|
|
1104
|
-
rangeEnd = rangeEnd === undefined ? segmentEnd : Math.max(rangeEnd, segmentEnd);
|
|
1105
|
-
searchStart = segmentEnd;
|
|
1106
|
-
}
|
|
1107
|
-
if (rangeStart === undefined || rangeEnd === undefined) {
|
|
1108
|
-
return initial;
|
|
1109
|
-
}
|
|
1110
|
-
return { start: rangeStart, end: rangeEnd };
|
|
1111
|
-
};
|
|
1112
|
-
const flush = () => {
|
|
1113
|
-
if (!currentGroup.length) {
|
|
1114
|
-
return;
|
|
1115
|
-
}
|
|
1116
|
-
const indices = currentGroup.map((token) => token.index);
|
|
1117
|
-
const initialRange = computeTokenRange(internal.input, internal.tokens, indices);
|
|
1118
|
-
const range = locateRange(currentGroup, initialRange);
|
|
1119
|
-
groups.push({ tokens: currentGroup, range });
|
|
1120
|
-
if (range) {
|
|
1121
|
-
minimumStart = Math.max(minimumStart, range.end);
|
|
1122
|
-
}
|
|
1123
|
-
currentGroup = [];
|
|
1124
|
-
previousIndex = undefined;
|
|
1125
|
-
};
|
|
1126
|
-
for (const token of leftoverTokens) {
|
|
1127
|
-
if (previousIndex !== undefined && token.index !== previousIndex + 1) {
|
|
1128
|
-
flush();
|
|
1129
|
-
}
|
|
1130
|
-
currentGroup.push(token);
|
|
1131
|
-
previousIndex = token.index;
|
|
1132
|
-
}
|
|
1133
|
-
flush();
|
|
1134
|
-
return groups;
|
|
1135
|
-
}
|
|
1136
|
-
function splitToken(token) {
|
|
1137
|
-
if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
|
|
1138
|
-
return [token];
|
|
1139
|
-
}
|
|
1140
|
-
const compactPoMeal = token.match(/^(po)(ac|pc|c)$/i);
|
|
1141
|
-
if (compactPoMeal) {
|
|
1142
|
-
const [, po, meal] = compactPoMeal;
|
|
1143
|
-
return [po, meal];
|
|
1144
|
-
}
|
|
1145
|
-
if (/^[A-Za-z]+$/.test(token)) {
|
|
1146
|
-
return [token];
|
|
1147
|
-
}
|
|
1148
|
-
const qRange = token.match(/^q([0-9]+(?:\.[0-9]+)?)-([0-9]+(?:\.[0-9]+)?)([A-Za-z]+)$/i);
|
|
1149
|
-
if (qRange) {
|
|
1150
|
-
const [, low, high, unit] = qRange;
|
|
1151
|
-
return [token.charAt(0), `${low}-${high}`, unit];
|
|
1152
|
-
}
|
|
1153
|
-
const match = token.match(/^([0-9]+(?:\.[0-9]+)?)([A-Za-z]+)$/);
|
|
1154
|
-
if (match) {
|
|
1155
|
-
const [, num, unit] = match;
|
|
1156
|
-
const compactPoMealUnit = unit.match(/^(po)(ac|pc|c)$/i);
|
|
1157
|
-
if (compactPoMealUnit) {
|
|
1158
|
-
const [, po, meal] = compactPoMealUnit;
|
|
1159
|
-
return [num, po, meal];
|
|
1160
|
-
}
|
|
1161
|
-
if (!/^x\d+/i.test(unit) && !/^q\d+/i.test(unit)) {
|
|
1162
|
-
return [num, unit];
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
return [token];
|
|
1166
|
-
}
|
|
1167
|
-
function mark(consumed, token) {
|
|
1168
|
-
consumed.add(token.index);
|
|
1169
|
-
}
|
|
1170
|
-
function addWhen(target, code) {
|
|
1171
|
-
if (!(0, array_1.arrayIncludes)(target, code)) {
|
|
1172
|
-
target.push(code);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
// Removing is slightly more work than adding because a clinician might repeat
|
|
1176
|
-
// the same token; trimming them all keeps downstream assertions tidy.
|
|
1177
|
-
function removeWhen(target, code) {
|
|
1178
|
-
let index = target.indexOf(code);
|
|
1179
|
-
while (index !== -1) {
|
|
1180
|
-
target.splice(index, 1);
|
|
1181
|
-
index = target.indexOf(code);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
const DEFAULT_EVENT_TIMING_WEIGHTS = {
|
|
1185
|
-
[types_1.EventTiming.Immediate]: 0,
|
|
1186
|
-
[types_1.EventTiming.Wake]: 6 * 3600,
|
|
1187
|
-
[types_1.EventTiming["After Sleep"]]: 6 * 3600 + 15 * 60,
|
|
1188
|
-
[types_1.EventTiming["Early Morning"]]: 7 * 3600,
|
|
1189
|
-
[types_1.EventTiming["Before Meal"]]: 7 * 3600 + 30 * 60,
|
|
1190
|
-
[types_1.EventTiming["Before Breakfast"]]: 7 * 3600 + 45 * 60,
|
|
1191
|
-
[types_1.EventTiming.Morning]: 8 * 3600,
|
|
1192
|
-
[types_1.EventTiming.Breakfast]: 8 * 3600 + 15 * 60,
|
|
1193
|
-
[types_1.EventTiming.Meal]: 8 * 3600 + 30 * 60,
|
|
1194
|
-
[types_1.EventTiming["After Breakfast"]]: 9 * 3600,
|
|
1195
|
-
[types_1.EventTiming["After Meal"]]: 9 * 3600 + 15 * 60,
|
|
1196
|
-
[types_1.EventTiming["Late Morning"]]: 10 * 3600 + 30 * 60,
|
|
1197
|
-
[types_1.EventTiming["Before Lunch"]]: 11 * 3600 + 45 * 60,
|
|
1198
|
-
[types_1.EventTiming.Noon]: 12 * 3600,
|
|
1199
|
-
[types_1.EventTiming.Lunch]: 12 * 3600 + 15 * 60,
|
|
1200
|
-
[types_1.EventTiming["After Lunch"]]: 12 * 3600 + 45 * 60,
|
|
1201
|
-
[types_1.EventTiming["Early Afternoon"]]: 13 * 3600 + 30 * 60,
|
|
1202
|
-
[types_1.EventTiming.Afternoon]: 15 * 3600,
|
|
1203
|
-
[types_1.EventTiming["Late Afternoon"]]: 16 * 3600 + 30 * 60,
|
|
1204
|
-
[types_1.EventTiming["Before Dinner"]]: 17 * 3600 + 30 * 60,
|
|
1205
|
-
[types_1.EventTiming.Dinner]: 18 * 3600,
|
|
1206
|
-
[types_1.EventTiming["After Dinner"]]: 19 * 3600,
|
|
1207
|
-
[types_1.EventTiming["Early Evening"]]: 19 * 3600 + 30 * 60,
|
|
1208
|
-
[types_1.EventTiming.Evening]: 20 * 3600,
|
|
1209
|
-
[types_1.EventTiming["Late Evening"]]: 21 * 3600,
|
|
1210
|
-
[types_1.EventTiming.Night]: 22 * 3600,
|
|
1211
|
-
[types_1.EventTiming["Before Sleep"]]: 22 * 3600 + 30 * 60,
|
|
1212
|
-
};
|
|
1213
|
-
function parseClockToSeconds(clock) {
|
|
1214
|
-
const match = clock.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
1215
|
-
if (!match) {
|
|
1216
|
-
return undefined;
|
|
1217
|
-
}
|
|
1218
|
-
const hour = Number(match[1]);
|
|
1219
|
-
const minute = Number(match[2]);
|
|
1220
|
-
const second = match[3] ? Number(match[3]) : 0;
|
|
1221
|
-
if (!Number.isFinite(hour) ||
|
|
1222
|
-
!Number.isFinite(minute) ||
|
|
1223
|
-
!Number.isFinite(second) ||
|
|
1224
|
-
hour < 0 ||
|
|
1225
|
-
hour > 23 ||
|
|
1226
|
-
minute < 0 ||
|
|
1227
|
-
minute > 59 ||
|
|
1228
|
-
second < 0 ||
|
|
1229
|
-
second > 59) {
|
|
1230
|
-
return undefined;
|
|
1231
|
-
}
|
|
1232
|
-
return hour * 3600 + minute * 60 + second;
|
|
1233
|
-
}
|
|
1234
|
-
function computeWhenWeight(code, options) {
|
|
1235
|
-
var _a, _b;
|
|
1236
|
-
const clock = (_a = options === null || options === void 0 ? void 0 : options.eventClock) === null || _a === void 0 ? void 0 : _a[code];
|
|
1237
|
-
if (clock) {
|
|
1238
|
-
const seconds = parseClockToSeconds(clock);
|
|
1239
|
-
if (seconds !== undefined) {
|
|
1240
|
-
return seconds;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return (_b = DEFAULT_EVENT_TIMING_WEIGHTS[code]) !== null && _b !== void 0 ? _b : 10000;
|
|
1244
|
-
}
|
|
1245
|
-
function sortWhenValues(internal, options) {
|
|
1246
|
-
if (internal.when.length < 2) {
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
const weighted = internal.when.map((code, index) => ({
|
|
1250
|
-
code,
|
|
1251
|
-
weight: computeWhenWeight(code, options),
|
|
1252
|
-
index,
|
|
1253
|
-
}));
|
|
1254
|
-
weighted.sort((a, b) => {
|
|
1255
|
-
if (a.weight !== b.weight) {
|
|
1256
|
-
return a.weight - b.weight;
|
|
1257
|
-
}
|
|
1258
|
-
return a.index - b.index;
|
|
1259
|
-
});
|
|
1260
|
-
internal.when.splice(0, internal.when.length, ...weighted.map((entry) => entry.code));
|
|
1261
|
-
}
|
|
1262
|
-
// Translate the requested expansion context into the appropriate sequence of
|
|
1263
|
-
// EventTiming values (e.g., AC -> ACM/ACD/ACV) for the detected frequency.
|
|
1264
|
-
function computeMealExpansions(base, frequency, pairPreference) {
|
|
1265
|
-
if (frequency < 1 || frequency > 4) {
|
|
1266
|
-
return undefined;
|
|
1267
|
-
}
|
|
1268
|
-
const bedtime = types_1.EventTiming["Before Sleep"];
|
|
1269
|
-
const beforePair = pairPreference === "breakfast+lunch"
|
|
1270
|
-
? [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Lunch"]]
|
|
1271
|
-
: [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Dinner"]];
|
|
1272
|
-
const afterPair = pairPreference === "breakfast+lunch"
|
|
1273
|
-
? [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Lunch"]]
|
|
1274
|
-
: [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Dinner"]];
|
|
1275
|
-
const withPair = pairPreference === "breakfast+lunch"
|
|
1276
|
-
? [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch]
|
|
1277
|
-
: [types_1.EventTiming.Breakfast, types_1.EventTiming.Dinner];
|
|
1278
|
-
if (base === "before") {
|
|
1279
|
-
if (frequency === 1)
|
|
1280
|
-
return [types_1.EventTiming["Before Breakfast"]];
|
|
1281
|
-
if (frequency === 2)
|
|
1282
|
-
return beforePair;
|
|
1283
|
-
if (frequency === 3) {
|
|
1284
|
-
return [
|
|
1285
|
-
types_1.EventTiming["Before Breakfast"],
|
|
1286
|
-
types_1.EventTiming["Before Lunch"],
|
|
1287
|
-
types_1.EventTiming["Before Dinner"]
|
|
1288
|
-
];
|
|
1289
|
-
}
|
|
1290
|
-
return [
|
|
1291
|
-
types_1.EventTiming["Before Breakfast"],
|
|
1292
|
-
types_1.EventTiming["Before Lunch"],
|
|
1293
|
-
types_1.EventTiming["Before Dinner"],
|
|
1294
|
-
bedtime
|
|
1295
|
-
];
|
|
1296
|
-
}
|
|
1297
|
-
if (base === "after") {
|
|
1298
|
-
if (frequency === 1)
|
|
1299
|
-
return [types_1.EventTiming["After Breakfast"]];
|
|
1300
|
-
if (frequency === 2)
|
|
1301
|
-
return afterPair;
|
|
1302
|
-
if (frequency === 3) {
|
|
1303
|
-
return [
|
|
1304
|
-
types_1.EventTiming["After Breakfast"],
|
|
1305
|
-
types_1.EventTiming["After Lunch"],
|
|
1306
|
-
types_1.EventTiming["After Dinner"]
|
|
1307
|
-
];
|
|
1308
|
-
}
|
|
1309
|
-
return [
|
|
1310
|
-
types_1.EventTiming["After Breakfast"],
|
|
1311
|
-
types_1.EventTiming["After Lunch"],
|
|
1312
|
-
types_1.EventTiming["After Dinner"],
|
|
1313
|
-
bedtime
|
|
1314
|
-
];
|
|
1315
|
-
}
|
|
1316
|
-
// base === "with"
|
|
1317
|
-
if (frequency === 1)
|
|
1318
|
-
return [types_1.EventTiming.Breakfast];
|
|
1319
|
-
if (frequency === 2)
|
|
1320
|
-
return withPair;
|
|
1321
|
-
if (frequency === 3) {
|
|
1322
|
-
return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner];
|
|
1323
|
-
}
|
|
1324
|
-
return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner, bedtime];
|
|
1325
|
-
}
|
|
1326
|
-
function reconcileMealTimingSpecificity(internal) {
|
|
1327
|
-
if (!internal.when.length) {
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
const convertSpecifics = (base, mappings) => {
|
|
1331
|
-
if (!(0, array_1.arrayIncludes)(internal.when, base)) {
|
|
1332
|
-
return;
|
|
1333
|
-
}
|
|
1334
|
-
let replaced = false;
|
|
1335
|
-
for (const [general, specific] of mappings) {
|
|
1336
|
-
if ((0, array_1.arrayIncludes)(internal.when, general)) {
|
|
1337
|
-
removeWhen(internal.when, general);
|
|
1338
|
-
addWhen(internal.when, specific);
|
|
1339
|
-
replaced = true;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
if (replaced) {
|
|
1343
|
-
removeWhen(internal.when, base);
|
|
1344
|
-
}
|
|
1345
|
-
};
|
|
1346
|
-
convertSpecifics(types_1.EventTiming["Before Meal"], [
|
|
1347
|
-
[types_1.EventTiming.Breakfast, types_1.EventTiming["Before Breakfast"]],
|
|
1348
|
-
[types_1.EventTiming.Lunch, types_1.EventTiming["Before Lunch"]],
|
|
1349
|
-
[types_1.EventTiming.Dinner, types_1.EventTiming["Before Dinner"]],
|
|
1350
|
-
]);
|
|
1351
|
-
convertSpecifics(types_1.EventTiming["After Meal"], [
|
|
1352
|
-
[types_1.EventTiming.Breakfast, types_1.EventTiming["After Breakfast"]],
|
|
1353
|
-
[types_1.EventTiming.Lunch, types_1.EventTiming["After Lunch"]],
|
|
1354
|
-
[types_1.EventTiming.Dinner, types_1.EventTiming["After Dinner"]],
|
|
1355
|
-
]);
|
|
1356
|
-
}
|
|
1357
|
-
// Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
|
|
1358
|
-
// EventTiming codes when the cadence or explicit meal abbreviations make the
|
|
1359
|
-
// intent obvious.
|
|
1360
|
-
function expandMealTimings(internal, options) {
|
|
1361
|
-
var _a, _b, _c;
|
|
1362
|
-
const allowSmartExpansion = (options === null || options === void 0 ? void 0 : options.smartMealExpansion) === true;
|
|
1363
|
-
if (!allowSmartExpansion) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
if (internal.when.some((code) => SPECIFIC_MEAL_TIMINGS.has(code))) {
|
|
1367
|
-
return;
|
|
1368
|
-
}
|
|
1369
|
-
const frequency = internal.frequency;
|
|
1370
|
-
if (!frequency || frequency < 1 || frequency > 4) {
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
const needsDefaultExpansion = internal.when.length === 0 && frequency >= 2;
|
|
1374
|
-
const hasBeforeMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["Before Meal"]);
|
|
1375
|
-
const hasAfterMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["After Meal"]);
|
|
1376
|
-
const hasWithMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming.Meal);
|
|
1377
|
-
const hasGeneralMealToken = hasBeforeMeal || hasAfterMeal || hasWithMeal;
|
|
1378
|
-
if (!hasGeneralMealToken && !needsDefaultExpansion) {
|
|
1379
|
-
return;
|
|
1380
|
-
}
|
|
1381
|
-
if (internal.period !== undefined &&
|
|
1382
|
-
internal.periodUnit !== undefined &&
|
|
1383
|
-
(internal.periodUnit !== types_1.FhirPeriodUnit.Day || internal.period !== 1)) {
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
if (internal.period !== undefined &&
|
|
1387
|
-
internal.periodUnit === undefined &&
|
|
1388
|
-
internal.period !== 1) {
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
if (internal.periodUnit && internal.periodUnit !== types_1.FhirPeriodUnit.Day) {
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
if (internal.frequencyMax !== undefined || internal.periodMax !== undefined) {
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
const pairPreference = (_a = options === null || options === void 0 ? void 0 : options.twoPerDayPair) !== null && _a !== void 0 ? _a : "breakfast+dinner";
|
|
1398
|
-
const replacements = [];
|
|
1399
|
-
const addReplacement = (general, base, removeGeneral) => {
|
|
1400
|
-
const specifics = computeMealExpansions(base, frequency, pairPreference);
|
|
1401
|
-
if (specifics) {
|
|
1402
|
-
replacements.push({ general, specifics, removeGeneral });
|
|
1403
|
-
}
|
|
1404
|
-
};
|
|
1405
|
-
if (hasBeforeMeal) {
|
|
1406
|
-
addReplacement(types_1.EventTiming["Before Meal"], "before", true);
|
|
1407
|
-
}
|
|
1408
|
-
if (hasAfterMeal) {
|
|
1409
|
-
addReplacement(types_1.EventTiming["After Meal"], "after", true);
|
|
1410
|
-
}
|
|
1411
|
-
if (hasWithMeal) {
|
|
1412
|
-
addReplacement(types_1.EventTiming.Meal, "with", true);
|
|
1413
|
-
}
|
|
1414
|
-
if (needsDefaultExpansion) {
|
|
1415
|
-
const relation = (_c = (_b = options === null || options === void 0 ? void 0 : options.context) === null || _b === void 0 ? void 0 : _b.mealRelation) !== null && _c !== void 0 ? _c : types_1.EventTiming.Meal;
|
|
1416
|
-
const base = relation === types_1.EventTiming["Before Meal"]
|
|
1417
|
-
? "before"
|
|
1418
|
-
: relation === types_1.EventTiming["After Meal"]
|
|
1419
|
-
? "after"
|
|
1420
|
-
: "with";
|
|
1421
|
-
addReplacement(relation, base, false);
|
|
1422
|
-
}
|
|
1423
|
-
for (const { general, specifics, removeGeneral } of replacements) {
|
|
1424
|
-
if (removeGeneral) {
|
|
1425
|
-
removeWhen(internal.when, general);
|
|
1426
|
-
}
|
|
1427
|
-
for (const specific of specifics) {
|
|
1428
|
-
addWhen(internal.when, specific);
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
function setRoute(internal, code, text) {
|
|
1433
|
-
internal.routeCode = code;
|
|
1434
|
-
internal.routeText = text !== null && text !== void 0 ? text : maps_1.ROUTE_TEXT[code];
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Convert hour-based values into minutes when fractional quantities appear so
|
|
1438
|
-
* the resulting FHIR repeat payloads avoid unwieldy decimals.
|
|
1439
|
-
*/
|
|
1440
|
-
function normalizePeriodValue(value, unit) {
|
|
1441
|
-
if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(value) || value < 1)) {
|
|
1442
|
-
return { value: Math.round(value * 60 * 1000) / 1000, unit: types_1.FhirPeriodUnit.Minute };
|
|
1443
|
-
}
|
|
1444
|
-
return { value, unit };
|
|
1445
|
-
}
|
|
1446
|
-
/**
|
|
1447
|
-
* Ensure ranges expressed in hours remain consistent when fractional values
|
|
1448
|
-
* demand conversion into minutes.
|
|
1449
|
-
*/
|
|
1450
|
-
function normalizePeriodRange(low, high, unit) {
|
|
1451
|
-
if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(low) || !Number.isInteger(high) || low < 1 || high < 1)) {
|
|
1452
|
-
return {
|
|
1453
|
-
low: Math.round(low * 60 * 1000) / 1000,
|
|
1454
|
-
high: Math.round(high * 60 * 1000) / 1000,
|
|
1455
|
-
unit: types_1.FhirPeriodUnit.Minute
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
return { low, high, unit };
|
|
1459
|
-
}
|
|
1460
|
-
function periodUnitSuffix(unit) {
|
|
1461
|
-
switch (unit) {
|
|
1462
|
-
case types_1.FhirPeriodUnit.Minute:
|
|
1463
|
-
return "min";
|
|
1464
|
-
case types_1.FhirPeriodUnit.Hour:
|
|
1465
|
-
return "h";
|
|
1466
|
-
case types_1.FhirPeriodUnit.Day:
|
|
1467
|
-
return "d";
|
|
1468
|
-
case types_1.FhirPeriodUnit.Week:
|
|
1469
|
-
return "wk";
|
|
1470
|
-
case types_1.FhirPeriodUnit.Month:
|
|
1471
|
-
return "mo";
|
|
1472
|
-
case types_1.FhirPeriodUnit.Year:
|
|
1473
|
-
return "a";
|
|
1474
|
-
default:
|
|
1475
|
-
return undefined;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
function maybeAssignTimingCode(internal, value, unit) {
|
|
1479
|
-
const suffix = periodUnitSuffix(unit);
|
|
1480
|
-
if (!suffix) {
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
const key = `q${value}${suffix}`;
|
|
1484
|
-
const descriptor = maps_1.TIMING_ABBREVIATIONS[key];
|
|
1485
|
-
if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.code) && !internal.timingCode) {
|
|
1486
|
-
internal.timingCode = descriptor.code;
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Apply the chosen period/unit pair and infer helpful timing codes when the
|
|
1491
|
-
* period clearly represents common cadences (daily/weekly/monthly).
|
|
1492
|
-
*/
|
|
1493
|
-
function applyPeriod(internal, period, unit) {
|
|
1494
|
-
var _a, _b, _c;
|
|
1495
|
-
const normalized = normalizePeriodValue(period, unit);
|
|
1496
|
-
internal.period = normalized.value;
|
|
1497
|
-
internal.periodUnit = normalized.unit;
|
|
1498
|
-
maybeAssignTimingCode(internal, normalized.value, normalized.unit);
|
|
1499
|
-
if (normalized.unit === types_1.FhirPeriodUnit.Day && normalized.value === 1) {
|
|
1500
|
-
internal.frequency = (_a = internal.frequency) !== null && _a !== void 0 ? _a : 1;
|
|
1501
|
-
}
|
|
1502
|
-
if (normalized.unit === types_1.FhirPeriodUnit.Week && normalized.value === 1) {
|
|
1503
|
-
internal.timingCode = (_b = internal.timingCode) !== null && _b !== void 0 ? _b : "WK";
|
|
1504
|
-
}
|
|
1505
|
-
if (normalized.unit === types_1.FhirPeriodUnit.Month && normalized.value === 1) {
|
|
1506
|
-
internal.timingCode = (_c = internal.timingCode) !== null && _c !== void 0 ? _c : "MO";
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Parse compact q-interval tokens like q30min, q0.5h, or q1w, optionally using
|
|
1511
|
-
* the following token as the unit if the compact token only carries the value.
|
|
1512
|
-
*/
|
|
1513
|
-
function tryParseCompactQ(internal, tokens, index) {
|
|
1514
|
-
const token = tokens[index];
|
|
1515
|
-
const lower = token.lower;
|
|
1516
|
-
const compact = lower.match(/^q([0-9]+(?:\.[0-9]+)?)([a-z]+)$/);
|
|
1517
|
-
if (compact) {
|
|
1518
|
-
const value = parseFloat(compact[1]);
|
|
1519
|
-
const unitCode = mapIntervalUnit(compact[2]);
|
|
1520
|
-
if (Number.isFinite(value) && unitCode) {
|
|
1521
|
-
applyPeriod(internal, value, unitCode);
|
|
1522
|
-
mark(internal.consumed, token);
|
|
1523
|
-
return true;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
const valueOnly = lower.match(/^q([0-9]+(?:\.[0-9]+)?)$/);
|
|
1527
|
-
if (valueOnly) {
|
|
1528
|
-
const unitToken = tokens[index + 1];
|
|
1529
|
-
if (!unitToken || internal.consumed.has(unitToken.index)) {
|
|
1530
|
-
return false;
|
|
1531
|
-
}
|
|
1532
|
-
const unitCode = mapIntervalUnit(unitToken.lower);
|
|
1533
|
-
if (!unitCode) {
|
|
1534
|
-
return false;
|
|
1535
|
-
}
|
|
1536
|
-
const value = parseFloat(valueOnly[1]);
|
|
1537
|
-
if (!Number.isFinite(value)) {
|
|
1538
|
-
return false;
|
|
1539
|
-
}
|
|
1540
|
-
applyPeriod(internal, value, unitCode);
|
|
1541
|
-
mark(internal.consumed, token);
|
|
1542
|
-
mark(internal.consumed, unitToken);
|
|
1543
|
-
return true;
|
|
1544
|
-
}
|
|
1545
|
-
return false;
|
|
1546
|
-
}
|
|
1547
|
-
function applyFrequencyDescriptor(internal, token, descriptor, options) {
|
|
1548
|
-
if (descriptor.discouraged) {
|
|
1549
|
-
const check = (0, safety_1.checkDiscouraged)(token.original, options);
|
|
1550
|
-
if (check.warning) {
|
|
1551
|
-
internal.warnings.push(check.warning);
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
if (descriptor.code) {
|
|
1555
|
-
internal.timingCode = descriptor.code;
|
|
1556
|
-
}
|
|
1557
|
-
if (descriptor.frequency !== undefined) {
|
|
1558
|
-
internal.frequency = descriptor.frequency;
|
|
1559
|
-
}
|
|
1560
|
-
if (descriptor.frequencyMax !== undefined) {
|
|
1561
|
-
internal.frequencyMax = descriptor.frequencyMax;
|
|
1562
|
-
}
|
|
1563
|
-
if (descriptor.period !== undefined) {
|
|
1564
|
-
internal.period = descriptor.period;
|
|
1565
|
-
}
|
|
1566
|
-
if (descriptor.periodMax !== undefined) {
|
|
1567
|
-
internal.periodMax = descriptor.periodMax;
|
|
1568
|
-
}
|
|
1569
|
-
if (descriptor.periodUnit) {
|
|
1570
|
-
internal.periodUnit = descriptor.periodUnit;
|
|
1571
|
-
}
|
|
1572
|
-
if (descriptor.when) {
|
|
1573
|
-
for (const w of descriptor.when) {
|
|
1574
|
-
addWhen(internal.when, w);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
mark(internal.consumed, token);
|
|
1578
|
-
}
|
|
1579
|
-
function applyWhenToken(internal, token, code) {
|
|
1580
|
-
addWhen(internal.when, code);
|
|
1581
|
-
mark(internal.consumed, token);
|
|
1582
|
-
}
|
|
1583
|
-
function isTimingAnchorOrPrefix(tokens, index, prnReasonStart) {
|
|
1584
|
-
const token = tokens[index];
|
|
1585
|
-
if (!token)
|
|
1586
|
-
return false;
|
|
1587
|
-
// Cautious handling of "sleep" in PRN zone
|
|
1588
|
-
if (prnReasonStart !== undefined && index >= prnReasonStart && token.lower === "sleep") {
|
|
1589
|
-
return false;
|
|
1590
|
-
}
|
|
1591
|
-
const lower = token.lower;
|
|
1592
|
-
const nextToken = tokens[index + 1];
|
|
1593
|
-
const comboKey = nextToken ? `${lower} ${nextToken.lower}` : undefined;
|
|
1594
|
-
return Boolean(maps_1.EVENT_TIMING_TOKENS[lower] ||
|
|
1595
|
-
maps_1.TIMING_ABBREVIATIONS[lower] ||
|
|
1596
|
-
(comboKey && COMBO_EVENT_TIMINGS[comboKey]) ||
|
|
1597
|
-
(lower === "pc" || lower === "ac" || lower === "after" || lower === "before") ||
|
|
1598
|
-
(isAtPrefixToken(lower) || lower === "on" || lower === "with") ||
|
|
1599
|
-
/^\d/.test(lower));
|
|
1600
|
-
}
|
|
1601
|
-
const DAY_SEQUENCE = [
|
|
1602
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1603
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1604
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1605
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1606
|
-
types_1.FhirDayOfWeek.Friday,
|
|
1607
|
-
types_1.FhirDayOfWeek.Saturday,
|
|
1608
|
-
types_1.FhirDayOfWeek.Sunday
|
|
1609
|
-
];
|
|
1610
|
-
const DAY_GROUP_TOKENS = {
|
|
1611
|
-
weekend: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1612
|
-
weekends: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1613
|
-
wknd: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1614
|
-
weekdays: [
|
|
1615
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1616
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1617
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1618
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1619
|
-
types_1.FhirDayOfWeek.Friday
|
|
1620
|
-
],
|
|
1621
|
-
weekday: [
|
|
1622
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1623
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1624
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1625
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1626
|
-
types_1.FhirDayOfWeek.Friday
|
|
1627
|
-
],
|
|
1628
|
-
workday: [
|
|
1629
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1630
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1631
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1632
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1633
|
-
types_1.FhirDayOfWeek.Friday
|
|
1634
|
-
],
|
|
1635
|
-
workdays: [
|
|
1636
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1637
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1638
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1639
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1640
|
-
types_1.FhirDayOfWeek.Friday
|
|
1641
|
-
],
|
|
1642
|
-
วันธรรมดา: [
|
|
1643
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1644
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1645
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1646
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1647
|
-
types_1.FhirDayOfWeek.Friday
|
|
1648
|
-
],
|
|
1649
|
-
วันทำงาน: [
|
|
1650
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1651
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1652
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1653
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1654
|
-
types_1.FhirDayOfWeek.Friday
|
|
1655
|
-
],
|
|
1656
|
-
วันหยุด: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1657
|
-
สุดสัปดาห์: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1658
|
-
เสาร์อาทิตย์: [types_1.FhirDayOfWeek.Saturday, types_1.FhirDayOfWeek.Sunday],
|
|
1659
|
-
จันทร์ถึงศุกร์: [
|
|
1660
|
-
types_1.FhirDayOfWeek.Monday,
|
|
1661
|
-
types_1.FhirDayOfWeek.Tuesday,
|
|
1662
|
-
types_1.FhirDayOfWeek.Wednesday,
|
|
1663
|
-
types_1.FhirDayOfWeek.Thursday,
|
|
1664
|
-
types_1.FhirDayOfWeek.Friday
|
|
1665
|
-
]
|
|
1666
|
-
};
|
|
1667
|
-
const DAY_RANGE_CONNECTOR_TOKENS = new Set(["-", "to", "through", "thru", "ถึง", "จนถึง"]);
|
|
1668
|
-
function addDayOfWeek(internal, day) {
|
|
1669
|
-
if (!(0, array_1.arrayIncludes)(internal.dayOfWeek, day)) {
|
|
1670
|
-
internal.dayOfWeek.push(day);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
function addDayOfWeekList(internal, days) {
|
|
1674
|
-
for (const day of days) {
|
|
1675
|
-
addDayOfWeek(internal, day);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
function expandDayRange(start, end) {
|
|
1679
|
-
const startIndex = DAY_SEQUENCE.indexOf(start);
|
|
1680
|
-
const endIndex = DAY_SEQUENCE.indexOf(end);
|
|
1681
|
-
if (startIndex < 0 || endIndex < 0) {
|
|
1682
|
-
return [start, end];
|
|
1683
|
-
}
|
|
1684
|
-
if (startIndex <= endIndex) {
|
|
1685
|
-
return DAY_SEQUENCE.slice(startIndex, endIndex + 1);
|
|
1686
|
-
}
|
|
1687
|
-
return [...DAY_SEQUENCE.slice(startIndex), ...DAY_SEQUENCE.slice(0, endIndex + 1)];
|
|
1688
|
-
}
|
|
1689
|
-
function resolveDayTokenDays(tokenLower) {
|
|
1690
|
-
const normalized = tokenLower.replace(/[.,;:]/g, "");
|
|
1691
|
-
const direct = maps_1.DAY_OF_WEEK_TOKENS[normalized];
|
|
1692
|
-
if (direct) {
|
|
1693
|
-
return [direct];
|
|
1694
|
-
}
|
|
1695
|
-
const grouped = DAY_GROUP_TOKENS[normalized];
|
|
1696
|
-
if (grouped) {
|
|
1697
|
-
return grouped.slice();
|
|
1698
|
-
}
|
|
1699
|
-
const rangeMatch = normalized.match(/^([^-–—~]+)[-–—~]([^-–—~]+)$/);
|
|
1700
|
-
if (rangeMatch) {
|
|
1701
|
-
const start = maps_1.DAY_OF_WEEK_TOKENS[rangeMatch[1]];
|
|
1702
|
-
const end = maps_1.DAY_OF_WEEK_TOKENS[rangeMatch[2]];
|
|
1703
|
-
if (start && end) {
|
|
1704
|
-
return expandDayRange(start, end);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
const compactConnectorRange = normalized.match(/^(.+?)(ถึง|จนถึง|to|through|thru)(.+)$/u);
|
|
1708
|
-
if (compactConnectorRange) {
|
|
1709
|
-
const start = maps_1.DAY_OF_WEEK_TOKENS[compactConnectorRange[1]];
|
|
1710
|
-
const end = maps_1.DAY_OF_WEEK_TOKENS[compactConnectorRange[3]];
|
|
1711
|
-
if (start && end) {
|
|
1712
|
-
return expandDayRange(start, end);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
return undefined;
|
|
1716
|
-
}
|
|
1717
|
-
function tryConsumeDayRangeTokens(internal, tokens, index) {
|
|
1718
|
-
const startToken = tokens[index];
|
|
1719
|
-
if (!startToken || internal.consumed.has(startToken.index)) {
|
|
1720
|
-
return 0;
|
|
1721
|
-
}
|
|
1722
|
-
const startDays = resolveDayTokenDays(normalizeTokenLower(startToken));
|
|
1723
|
-
if (!startDays || startDays.length !== 1) {
|
|
1724
|
-
return 0;
|
|
1725
|
-
}
|
|
1726
|
-
const connectorToken = tokens[index + 1];
|
|
1727
|
-
const endToken = tokens[index + 2];
|
|
1728
|
-
if (!connectorToken ||
|
|
1729
|
-
!endToken ||
|
|
1730
|
-
internal.consumed.has(connectorToken.index) ||
|
|
1731
|
-
internal.consumed.has(endToken.index)) {
|
|
1732
|
-
return 0;
|
|
1733
|
-
}
|
|
1734
|
-
const connector = normalizeTokenLower(connectorToken);
|
|
1735
|
-
if (!DAY_RANGE_CONNECTOR_TOKENS.has(connector)) {
|
|
1736
|
-
return 0;
|
|
1737
|
-
}
|
|
1738
|
-
const endDays = resolveDayTokenDays(normalizeTokenLower(endToken));
|
|
1739
|
-
if (!endDays || endDays.length !== 1) {
|
|
1740
|
-
return 0;
|
|
1741
|
-
}
|
|
1742
|
-
const expanded = expandDayRange(startDays[0], endDays[0]);
|
|
1743
|
-
addDayOfWeekList(internal, expanded);
|
|
1744
|
-
mark(internal.consumed, startToken);
|
|
1745
|
-
mark(internal.consumed, connectorToken);
|
|
1746
|
-
mark(internal.consumed, endToken);
|
|
1747
|
-
return 3;
|
|
1748
|
-
}
|
|
1749
|
-
function parseAnchorSequence(internal, tokens, index, prefixCode) {
|
|
1750
|
-
var _a;
|
|
1751
|
-
const token = tokens[index];
|
|
1752
|
-
let converted = 0;
|
|
1753
|
-
for (let lookahead = index + 1; lookahead < tokens.length; lookahead++) {
|
|
1754
|
-
const nextToken = tokens[lookahead];
|
|
1755
|
-
if (internal.consumed.has(nextToken.index)) {
|
|
1756
|
-
continue;
|
|
1757
|
-
}
|
|
1758
|
-
const lower = normalizeTokenLower(nextToken);
|
|
1759
|
-
if (MEAL_CONTEXT_CONNECTORS.has(lower) || lower === ",") {
|
|
1760
|
-
mark(internal.consumed, nextToken);
|
|
1761
|
-
continue;
|
|
1762
|
-
}
|
|
1763
|
-
const rangeConsumed = tryConsumeDayRangeTokens(internal, tokens, lookahead);
|
|
1764
|
-
if (rangeConsumed > 0) {
|
|
1765
|
-
converted++;
|
|
1766
|
-
lookahead += rangeConsumed - 1;
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
const days = resolveDayTokenDays(lower);
|
|
1770
|
-
if (days) {
|
|
1771
|
-
addDayOfWeekList(internal, days);
|
|
1772
|
-
mark(internal.consumed, nextToken);
|
|
1773
|
-
converted++;
|
|
1774
|
-
continue;
|
|
1775
|
-
}
|
|
1776
|
-
const meal = maps_1.MEAL_KEYWORDS[lower];
|
|
1777
|
-
if (meal) {
|
|
1778
|
-
const whenCode = prefixCode === types_1.EventTiming["After Meal"]
|
|
1779
|
-
? meal.pc
|
|
1780
|
-
: prefixCode === types_1.EventTiming["Before Meal"]
|
|
1781
|
-
? meal.ac
|
|
1782
|
-
: ((_a = maps_1.EVENT_TIMING_TOKENS[lower]) !== null && _a !== void 0 ? _a : meal.pc); // fallback to general or conservative default
|
|
1783
|
-
addWhen(internal.when, whenCode);
|
|
1784
|
-
mark(internal.consumed, nextToken);
|
|
1785
|
-
converted++;
|
|
1786
|
-
continue;
|
|
1787
|
-
}
|
|
1788
|
-
const whenCode = maps_1.EVENT_TIMING_TOKENS[lower];
|
|
1789
|
-
if (whenCode) {
|
|
1790
|
-
if (prefixCode && !meal) {
|
|
1791
|
-
// if we have pc/ac, we only want to follow it with explicit meals
|
|
1792
|
-
// to avoid over-consuming anchors that should be separate (like 'pc hs')
|
|
1793
|
-
break;
|
|
1794
|
-
}
|
|
1795
|
-
addWhen(internal.when, whenCode);
|
|
1796
|
-
mark(internal.consumed, nextToken);
|
|
1797
|
-
converted++;
|
|
1798
|
-
continue;
|
|
1799
|
-
}
|
|
1800
|
-
break;
|
|
1801
|
-
}
|
|
1802
|
-
if (converted > 0) {
|
|
1803
|
-
mark(internal.consumed, token);
|
|
1804
|
-
return true;
|
|
1805
|
-
}
|
|
1806
|
-
if (prefixCode) {
|
|
1807
|
-
applyWhenToken(internal, token, prefixCode);
|
|
1808
|
-
return true;
|
|
1809
|
-
}
|
|
1810
|
-
return false;
|
|
1811
|
-
}
|
|
1812
|
-
function parseSeparatedInterval(internal, tokens, index, options) {
|
|
1813
|
-
const token = tokens[index];
|
|
1814
|
-
const next = tokens[index + 1];
|
|
1815
|
-
if (!next || internal.consumed.has(next.index)) {
|
|
1816
|
-
return false;
|
|
1817
|
-
}
|
|
1818
|
-
const after = tokens[index + 2];
|
|
1819
|
-
const lowerNext = next.lower;
|
|
1820
|
-
const range = parseNumericRange(lowerNext);
|
|
1821
|
-
if (range) {
|
|
1822
|
-
const unitToken = after;
|
|
1823
|
-
if (!unitToken) {
|
|
1824
|
-
return false;
|
|
1825
|
-
}
|
|
1826
|
-
const unitCode = mapIntervalUnit(unitToken.lower);
|
|
1827
|
-
if (!unitCode) {
|
|
1828
|
-
return false;
|
|
1829
|
-
}
|
|
1830
|
-
const normalized = normalizePeriodRange(range.low, range.high, unitCode);
|
|
1831
|
-
internal.period = normalized.low;
|
|
1832
|
-
internal.periodMax = normalized.high;
|
|
1833
|
-
internal.periodUnit = normalized.unit;
|
|
1834
|
-
mark(internal.consumed, token);
|
|
1835
|
-
mark(internal.consumed, next);
|
|
1836
|
-
mark(internal.consumed, unitToken);
|
|
1837
|
-
return true;
|
|
1838
|
-
}
|
|
1839
|
-
const isNumber = /^[0-9]+(?:\.[0-9]+)?$/.test(lowerNext);
|
|
1840
|
-
if (!isNumber) {
|
|
1841
|
-
const unitCode = mapIntervalUnit(lowerNext);
|
|
1842
|
-
if (unitCode) {
|
|
1843
|
-
mark(internal.consumed, token);
|
|
1844
|
-
mark(internal.consumed, next);
|
|
1845
|
-
applyPeriod(internal, 1, unitCode);
|
|
1846
|
-
return true;
|
|
1847
|
-
}
|
|
1848
|
-
return false;
|
|
1849
|
-
}
|
|
1850
|
-
const unitToken = after;
|
|
1851
|
-
if (!unitToken) {
|
|
1852
|
-
return false;
|
|
1853
|
-
}
|
|
1854
|
-
const unitCode = mapIntervalUnit(unitToken.lower);
|
|
1855
|
-
if (!unitCode) {
|
|
1856
|
-
return false;
|
|
1857
|
-
}
|
|
1858
|
-
const value = parseFloat(next.original);
|
|
1859
|
-
applyPeriod(internal, value, unitCode);
|
|
1860
|
-
mark(internal.consumed, token);
|
|
1861
|
-
mark(internal.consumed, next);
|
|
1862
|
-
mark(internal.consumed, unitToken);
|
|
1863
|
-
return true;
|
|
1864
|
-
}
|
|
1865
|
-
function mapIntervalUnit(token) {
|
|
1866
|
-
if (token === "min" ||
|
|
1867
|
-
token === "mins" ||
|
|
1868
|
-
token === "minute" ||
|
|
1869
|
-
token === "minutes" ||
|
|
1870
|
-
token === "m") {
|
|
1871
|
-
return types_1.FhirPeriodUnit.Minute;
|
|
1872
|
-
}
|
|
1873
|
-
if (token === "h" || token === "hr" || token === "hrs" || token === "hour" || token === "hours") {
|
|
1874
|
-
return types_1.FhirPeriodUnit.Hour;
|
|
1875
|
-
}
|
|
1876
|
-
if (token === "d" || token === "day" || token === "days") {
|
|
1877
|
-
return types_1.FhirPeriodUnit.Day;
|
|
1878
|
-
}
|
|
1879
|
-
if (token === "wk" || token === "w" || token === "week" || token === "weeks") {
|
|
1880
|
-
return types_1.FhirPeriodUnit.Week;
|
|
1881
|
-
}
|
|
1882
|
-
if (token === "mo" || token === "month" || token === "months") {
|
|
1883
|
-
return types_1.FhirPeriodUnit.Month;
|
|
1884
|
-
}
|
|
1885
|
-
return undefined;
|
|
1886
|
-
}
|
|
1887
|
-
function mapFrequencyAdverb(token) {
|
|
1888
|
-
return FREQUENCY_ADVERB_UNITS[token];
|
|
1889
|
-
}
|
|
1890
|
-
function parseNumericRange(token) {
|
|
1891
|
-
const rangeMatch = token.match(/^([0-9]+(?:\.[0-9]+)?)-([0-9]+(?:\.[0-9]+)?)$/);
|
|
1892
|
-
if (!rangeMatch) {
|
|
1893
|
-
return undefined;
|
|
1894
|
-
}
|
|
1895
|
-
const low = parseFloat(rangeMatch[1]);
|
|
1896
|
-
const high = parseFloat(rangeMatch[2]);
|
|
1897
|
-
if (!Number.isFinite(low) || !Number.isFinite(high)) {
|
|
1898
|
-
return undefined;
|
|
1899
|
-
}
|
|
1900
|
-
return { low, high };
|
|
1901
|
-
}
|
|
1902
|
-
function applyCountLimit(internal, value) {
|
|
1903
|
-
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
1904
|
-
return false;
|
|
1905
|
-
}
|
|
1906
|
-
if (internal.count !== undefined) {
|
|
1907
|
-
return false;
|
|
1908
|
-
}
|
|
1909
|
-
const rounded = Math.round(value);
|
|
1910
|
-
if (rounded <= 0) {
|
|
1911
|
-
return false;
|
|
1912
|
-
}
|
|
1913
|
-
internal.count = rounded;
|
|
1914
|
-
return true;
|
|
1915
|
-
}
|
|
1916
|
-
const DOSE_SCALE_MULTIPLIERS = {
|
|
1917
|
-
k: 1000,
|
|
1918
|
-
thousand: 1000,
|
|
1919
|
-
m: 1000000,
|
|
1920
|
-
mn: 1000000,
|
|
1921
|
-
mio: 1000000,
|
|
1922
|
-
million: 1000000,
|
|
1923
|
-
b: 1000000000,
|
|
1924
|
-
bn: 1000000000,
|
|
1925
|
-
billion: 1000000000
|
|
1926
|
-
};
|
|
1927
|
-
function resolveUnitTokenAt(tokens, index, consumed, options) {
|
|
1928
|
-
const token = tokens[index];
|
|
1929
|
-
if (!token || consumed.has(token.index)) {
|
|
1930
|
-
return undefined;
|
|
1931
|
-
}
|
|
1932
|
-
const normalized = normalizeTokenLower(token);
|
|
1933
|
-
const direct = normalizeUnit(normalized, options);
|
|
1934
|
-
if (direct) {
|
|
1935
|
-
return { unit: direct, consumedIndices: [index] };
|
|
1936
|
-
}
|
|
1937
|
-
if (normalized === "international") {
|
|
1938
|
-
const nextToken = tokens[index + 1];
|
|
1939
|
-
if (!nextToken || consumed.has(nextToken.index)) {
|
|
1940
|
-
return undefined;
|
|
1941
|
-
}
|
|
1942
|
-
const nextNormalized = normalizeTokenLower(nextToken);
|
|
1943
|
-
if (nextNormalized === "unit" ||
|
|
1944
|
-
nextNormalized === "units" ||
|
|
1945
|
-
nextNormalized === "u" ||
|
|
1946
|
-
nextNormalized === "iu" ||
|
|
1947
|
-
nextNormalized === "ius") {
|
|
1948
|
-
return { unit: "IU", consumedIndices: [index, index + 1] };
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
return undefined;
|
|
1952
|
-
}
|
|
1953
|
-
function resolveNumericDoseUnit(tokens, numberIndex, value, consumed, options) {
|
|
1954
|
-
const directUnit = resolveUnitTokenAt(tokens, numberIndex + 1, consumed, options);
|
|
1955
|
-
if (directUnit) {
|
|
1956
|
-
return {
|
|
1957
|
-
doseValue: value,
|
|
1958
|
-
unit: directUnit.unit,
|
|
1959
|
-
consumedIndices: directUnit.consumedIndices
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
const scaleToken = tokens[numberIndex + 1];
|
|
1963
|
-
if (!scaleToken || consumed.has(scaleToken.index)) {
|
|
1964
|
-
return { doseValue: value, consumedIndices: [] };
|
|
1965
|
-
}
|
|
1966
|
-
const multiplier = DOSE_SCALE_MULTIPLIERS[normalizeTokenLower(scaleToken)];
|
|
1967
|
-
if (!multiplier) {
|
|
1968
|
-
return { doseValue: value, consumedIndices: [] };
|
|
1969
|
-
}
|
|
1970
|
-
const scaledUnit = resolveUnitTokenAt(tokens, numberIndex + 2, consumed, options);
|
|
1971
|
-
if (!scaledUnit) {
|
|
1972
|
-
return { doseValue: value, consumedIndices: [] };
|
|
1973
|
-
}
|
|
1974
|
-
return {
|
|
1975
|
-
doseValue: value * multiplier,
|
|
1976
|
-
unit: scaledUnit.unit,
|
|
1977
|
-
consumedIndices: [numberIndex + 1, ...scaledUnit.consumedIndices]
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
function parseInternal(input, options) {
|
|
1981
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1982
|
-
const tokens = tokenize(input);
|
|
1983
|
-
const internal = {
|
|
1984
|
-
input,
|
|
1985
|
-
tokens,
|
|
1986
|
-
consumed: new Set(),
|
|
1987
|
-
dayOfWeek: [],
|
|
1988
|
-
when: [],
|
|
1989
|
-
warnings: [],
|
|
1990
|
-
siteTokenIndices: new Set(),
|
|
1991
|
-
siteLookups: [],
|
|
1992
|
-
customSiteHints: buildCustomSiteHints(options === null || options === void 0 ? void 0 : options.siteCodeMap),
|
|
1993
|
-
prnReasonLookups: [],
|
|
1994
|
-
additionalInstructions: []
|
|
1995
|
-
};
|
|
1996
|
-
const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
|
|
1997
|
-
const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
|
|
1998
|
-
? new Map((0, object_1.objectEntries)(options.routeMap).map(([key, value]) => [
|
|
1999
|
-
key.toLowerCase(),
|
|
2000
|
-
value
|
|
2001
|
-
]))
|
|
2002
|
-
: undefined;
|
|
2003
|
-
const customRouteDescriptorMap = customRouteMap
|
|
2004
|
-
? new Map(Array.from(customRouteMap.entries())
|
|
2005
|
-
.map(([key, value]) => [normalizeRouteDescriptorPhrase(key), value])
|
|
2006
|
-
.filter(([normalized]) => normalized.length > 0))
|
|
2007
|
-
: undefined;
|
|
2008
|
-
if (tokens.length === 0) {
|
|
2009
|
-
return internal;
|
|
2010
|
-
}
|
|
2011
|
-
// PRN detection
|
|
2012
|
-
let prnReasonStart;
|
|
2013
|
-
const prnSiteSuffixIndices = new Set();
|
|
2014
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2015
|
-
const token = tokens[i];
|
|
2016
|
-
if (token.lower === "prn") {
|
|
2017
|
-
internal.asNeeded = true;
|
|
2018
|
-
mark(internal.consumed, token);
|
|
2019
|
-
let reasonIndex = i + 1;
|
|
2020
|
-
if (((_b = tokens[reasonIndex]) === null || _b === void 0 ? void 0 : _b.lower) === "for") {
|
|
2021
|
-
mark(internal.consumed, tokens[reasonIndex]);
|
|
2022
|
-
reasonIndex += 1;
|
|
2023
|
-
}
|
|
2024
|
-
prnReasonStart = reasonIndex;
|
|
2025
|
-
break;
|
|
2026
|
-
}
|
|
2027
|
-
if (token.lower === "as" && ((_c = tokens[i + 1]) === null || _c === void 0 ? void 0 : _c.lower) === "needed") {
|
|
2028
|
-
internal.asNeeded = true;
|
|
2029
|
-
mark(internal.consumed, token);
|
|
2030
|
-
mark(internal.consumed, tokens[i + 1]);
|
|
2031
|
-
let reasonIndex = i + 2;
|
|
2032
|
-
if (((_d = tokens[reasonIndex]) === null || _d === void 0 ? void 0 : _d.lower) === "for") {
|
|
2033
|
-
mark(internal.consumed, tokens[reasonIndex]);
|
|
2034
|
-
reasonIndex += 1;
|
|
2035
|
-
}
|
|
2036
|
-
prnReasonStart = reasonIndex;
|
|
2037
|
-
break;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
// Multiplicative tokens like 1x3
|
|
2041
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2042
|
-
const token = tokens[i];
|
|
2043
|
-
if (internal.consumed.has(token.index))
|
|
2044
|
-
continue;
|
|
2045
|
-
const combined = token.lower.match(/^([0-9]+(?:\.[0-9]+)?)[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
2046
|
-
if (combined) {
|
|
2047
|
-
const dose = parseFloat(combined[1]);
|
|
2048
|
-
const freq = parseFloat(combined[2]);
|
|
2049
|
-
if (internal.dose === undefined) {
|
|
2050
|
-
internal.dose = dose;
|
|
2051
|
-
}
|
|
2052
|
-
internal.frequency = freq;
|
|
2053
|
-
internal.period = 1;
|
|
2054
|
-
internal.periodUnit = types_1.FhirPeriodUnit.Day;
|
|
2055
|
-
mark(internal.consumed, token);
|
|
2056
|
-
continue;
|
|
2057
|
-
}
|
|
2058
|
-
const hasNumericDoseBefore = () => {
|
|
2059
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
2060
|
-
const prev = tokens[j];
|
|
2061
|
-
if (!prev) {
|
|
2062
|
-
continue;
|
|
2063
|
-
}
|
|
2064
|
-
if (internal.consumed.has(prev.index)) {
|
|
2065
|
-
continue;
|
|
2066
|
-
}
|
|
2067
|
-
if (/^[0-9]+(?:\.[0-9]+)?$/.test(prev.lower)) {
|
|
2068
|
-
return true;
|
|
2069
|
-
}
|
|
2070
|
-
if (normalizeUnit(prev.lower, options)) {
|
|
2071
|
-
continue;
|
|
2072
|
-
}
|
|
2073
|
-
break;
|
|
2074
|
-
}
|
|
2075
|
-
return false;
|
|
2076
|
-
};
|
|
2077
|
-
if (internal.frequency === undefined && hasNumericDoseBefore()) {
|
|
2078
|
-
const prefix = token.lower.match(/^[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
2079
|
-
if (prefix) {
|
|
2080
|
-
const freq = parseFloat(prefix[1]);
|
|
2081
|
-
if (Number.isFinite(freq)) {
|
|
2082
|
-
internal.frequency = freq;
|
|
2083
|
-
internal.period = 1;
|
|
2084
|
-
internal.periodUnit = types_1.FhirPeriodUnit.Day;
|
|
2085
|
-
mark(internal.consumed, token);
|
|
2086
|
-
continue;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
if (token.lower === "x" || token.lower === "*") {
|
|
2090
|
-
const next = tokens[i + 1];
|
|
2091
|
-
if (next &&
|
|
2092
|
-
!internal.consumed.has(next.index) &&
|
|
2093
|
-
/^[0-9]+(?:\.[0-9]+)?$/.test(next.lower)) {
|
|
2094
|
-
const freq = parseFloat(next.original);
|
|
2095
|
-
if (Number.isFinite(freq)) {
|
|
2096
|
-
internal.frequency = freq;
|
|
2097
|
-
internal.period = 1;
|
|
2098
|
-
internal.periodUnit = types_1.FhirPeriodUnit.Day;
|
|
2099
|
-
mark(internal.consumed, token);
|
|
2100
|
-
mark(internal.consumed, next);
|
|
2101
|
-
continue;
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
const applyRouteDescriptor = (code, text) => {
|
|
2108
|
-
if (internal.routeCode && internal.routeCode !== code) {
|
|
2109
|
-
return false;
|
|
2110
|
-
}
|
|
2111
|
-
setRoute(internal, code, text);
|
|
2112
|
-
return true;
|
|
2113
|
-
};
|
|
2114
|
-
const maybeApplyRouteDescriptor = (phrase) => {
|
|
2115
|
-
if (!phrase) {
|
|
2116
|
-
return false;
|
|
2117
|
-
}
|
|
2118
|
-
const normalized = phrase.trim().toLowerCase();
|
|
2119
|
-
if (!normalized) {
|
|
2120
|
-
return false;
|
|
2121
|
-
}
|
|
2122
|
-
const customCode = customRouteMap === null || customRouteMap === void 0 ? void 0 : customRouteMap.get(normalized);
|
|
2123
|
-
if (customCode) {
|
|
2124
|
-
if (applyRouteDescriptor(customCode)) {
|
|
2125
|
-
return true;
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
const synonym = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized];
|
|
2129
|
-
if (synonym) {
|
|
2130
|
-
if (applyRouteDescriptor(synonym.code, synonym.text)) {
|
|
2131
|
-
return true;
|
|
2132
|
-
}
|
|
2133
|
-
}
|
|
2134
|
-
const normalizedDescriptor = normalizeRouteDescriptorPhrase(normalized);
|
|
2135
|
-
if (normalizedDescriptor && normalizedDescriptor !== normalized) {
|
|
2136
|
-
const customDescriptorCode = customRouteDescriptorMap === null || customRouteDescriptorMap === void 0 ? void 0 : customRouteDescriptorMap.get(normalizedDescriptor);
|
|
2137
|
-
if (customDescriptorCode) {
|
|
2138
|
-
if (applyRouteDescriptor(customDescriptorCode)) {
|
|
2139
|
-
return true;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
const fallbackSynonym = DEFAULT_ROUTE_DESCRIPTOR_SYNONYMS.get(normalizedDescriptor);
|
|
2143
|
-
if (fallbackSynonym) {
|
|
2144
|
-
if (applyRouteDescriptor(fallbackSynonym.code, fallbackSynonym.text)) {
|
|
2145
|
-
return true;
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
return false;
|
|
2150
|
-
};
|
|
2151
|
-
// Process tokens sequentially
|
|
2152
|
-
const tryRouteSynonym = (startIndex) => {
|
|
2153
|
-
if (prnReasonStart !== undefined && startIndex >= prnReasonStart) {
|
|
2154
|
-
return false;
|
|
2155
|
-
}
|
|
2156
|
-
const maxSpan = Math.min(24, tokens.length - startIndex);
|
|
2157
|
-
for (let span = maxSpan; span >= 1; span--) {
|
|
2158
|
-
const slice = tokens.slice(startIndex, startIndex + span);
|
|
2159
|
-
if (slice.some((part) => internal.consumed.has(part.index))) {
|
|
2160
|
-
continue;
|
|
2161
|
-
}
|
|
2162
|
-
const normalizedParts = slice.filter((part) => !/^[;:(),]+$/.test(part.lower));
|
|
2163
|
-
const phrase = normalizedParts.map((part) => part.lower).join(" ");
|
|
2164
|
-
const customCode = customRouteMap === null || customRouteMap === void 0 ? void 0 : customRouteMap.get(phrase);
|
|
2165
|
-
const synonym = customCode
|
|
2166
|
-
? { code: customCode, text: maps_1.ROUTE_TEXT[customCode] }
|
|
2167
|
-
: maps_1.DEFAULT_ROUTE_SYNONYMS[phrase];
|
|
2168
|
-
if (synonym) {
|
|
2169
|
-
if (phrase === "in" && slice.length === 1) {
|
|
2170
|
-
if (internal.routeCode) {
|
|
2171
|
-
continue;
|
|
2172
|
-
}
|
|
2173
|
-
const prevToken = tokens[startIndex - 1];
|
|
2174
|
-
if (prevToken && !internal.consumed.has(prevToken.index)) {
|
|
2175
|
-
continue;
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
setRoute(internal, synonym.code, synonym.text);
|
|
2179
|
-
for (const part of slice) {
|
|
2180
|
-
mark(internal.consumed, part);
|
|
2181
|
-
if (isBodySiteHint(part.lower, internal.customSiteHints)) {
|
|
2182
|
-
internal.siteTokenIndices.add(part.index);
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
return true;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
return false;
|
|
2189
|
-
};
|
|
2190
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2191
|
-
const token = tokens[i];
|
|
2192
|
-
if (internal.consumed.has(token.index)) {
|
|
2193
|
-
continue;
|
|
2194
|
-
}
|
|
2195
|
-
const normalizedLower = normalizeTokenLower(token);
|
|
2196
|
-
if (token.lower === "bld" || token.lower === "b-l-d") {
|
|
2197
|
-
const check = (0, safety_1.checkDiscouraged)(token.original, options);
|
|
2198
|
-
if (check.warning) {
|
|
2199
|
-
internal.warnings.push(check.warning);
|
|
2200
|
-
}
|
|
2201
|
-
applyWhenToken(internal, token, types_1.EventTiming.Meal);
|
|
2202
|
-
continue;
|
|
2203
|
-
}
|
|
2204
|
-
if (token.lower === "q" || token.lower === "every" || token.lower === "each") {
|
|
2205
|
-
if (parseSeparatedInterval(internal, tokens, i, options)) {
|
|
2206
|
-
continue;
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
if (tryParseTimeBasedSchedule(internal, tokens, i)) {
|
|
2210
|
-
continue;
|
|
2211
|
-
}
|
|
2212
|
-
if (tryParseNumericCadence(internal, tokens, i)) {
|
|
2213
|
-
continue;
|
|
2214
|
-
}
|
|
2215
|
-
const eyeSite = EYE_SITE_TOKENS[normalizedLower];
|
|
2216
|
-
const treatEyeTokenAsSite = eyeSite
|
|
2217
|
-
? shouldTreatEyeTokenAsSite(internal, tokens, i, context)
|
|
2218
|
-
: false;
|
|
2219
|
-
if (normalizedLower === "od") {
|
|
2220
|
-
const descriptor = maps_1.TIMING_ABBREVIATIONS.od;
|
|
2221
|
-
if (descriptor &&
|
|
2222
|
-
shouldInterpretOdAsOnceDaily(internal, tokens, i, treatEyeTokenAsSite)) {
|
|
2223
|
-
applyFrequencyDescriptor(internal, token, descriptor, options);
|
|
2224
|
-
continue;
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
// Frequency abbreviation map
|
|
2228
|
-
const freqDescriptor = normalizedLower === "od"
|
|
2229
|
-
? undefined
|
|
2230
|
-
: (_e = maps_1.TIMING_ABBREVIATIONS[token.lower]) !== null && _e !== void 0 ? _e : maps_1.TIMING_ABBREVIATIONS[normalizedLower];
|
|
2231
|
-
if (freqDescriptor) {
|
|
2232
|
-
applyFrequencyDescriptor(internal, token, freqDescriptor, options);
|
|
2233
|
-
continue;
|
|
2234
|
-
}
|
|
2235
|
-
if (tryParseCompactQ(internal, tokens, i)) {
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
// Skip connectors if they are followed by recognized timing tokens or prefixes
|
|
2239
|
-
if (MEAL_CONTEXT_CONNECTORS.has(token.lower) || token.lower === ",") {
|
|
2240
|
-
if (isTimingAnchorOrPrefix(tokens, i + 1, prnReasonStart)) {
|
|
2241
|
-
mark(internal.consumed, token);
|
|
2242
|
-
continue;
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
// Event timing tokens
|
|
2246
|
-
const nextToken = tokens[i + 1];
|
|
2247
|
-
if (nextToken && !internal.consumed.has(nextToken.index)) {
|
|
2248
|
-
const lowerNext = nextToken.lower;
|
|
2249
|
-
const combo = `${token.lower} ${lowerNext}`;
|
|
2250
|
-
const comboWhen = (_f = COMBO_EVENT_TIMINGS[combo]) !== null && _f !== void 0 ? _f : maps_1.EVENT_TIMING_TOKENS[combo];
|
|
2251
|
-
if (comboWhen) {
|
|
2252
|
-
applyWhenToken(internal, token, comboWhen);
|
|
2253
|
-
mark(internal.consumed, nextToken);
|
|
2254
|
-
continue;
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
if (token.lower === "pc" || token.lower === "ac" || token.lower === "after" || token.lower === "before") {
|
|
2258
|
-
parseAnchorSequence(internal, tokens, i, (token.lower === "pc" || token.lower === "after")
|
|
2259
|
-
? types_1.EventTiming["After Meal"]
|
|
2260
|
-
: types_1.EventTiming["Before Meal"]);
|
|
2261
|
-
continue;
|
|
2262
|
-
}
|
|
2263
|
-
if (isAtPrefixToken(token.lower) || token.lower === "on" || token.lower === "with") {
|
|
2264
|
-
if (tryParseTimeBasedSchedule(internal, tokens, i)) {
|
|
2265
|
-
continue;
|
|
2266
|
-
}
|
|
2267
|
-
if (parseAnchorSequence(internal, tokens, i)) {
|
|
2268
|
-
continue;
|
|
2269
|
-
}
|
|
2270
|
-
// If none of the above consume it, and it's a known anchor prefix, mark it
|
|
2271
|
-
// but only if it's not "with" which might be part of other phrases later.
|
|
2272
|
-
if (token.lower !== "with") {
|
|
2273
|
-
mark(internal.consumed, token);
|
|
2274
|
-
continue;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
const customWhen = (_g = options === null || options === void 0 ? void 0 : options.whenMap) === null || _g === void 0 ? void 0 : _g[token.lower];
|
|
2278
|
-
if (customWhen) {
|
|
2279
|
-
applyWhenToken(internal, token, customWhen);
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
const whenCode = maps_1.EVENT_TIMING_TOKENS[token.lower];
|
|
2283
|
-
if (whenCode) {
|
|
2284
|
-
// If we are in the PRN zone, be cautious about common reason words like "sleep"
|
|
2285
|
-
// unless they were already handled by combo/anchor logic (which happens above).
|
|
2286
|
-
if (prnReasonStart !== undefined && i >= prnReasonStart && token.lower === "sleep") {
|
|
2287
|
-
// Leave for PRN reason
|
|
2288
|
-
}
|
|
2289
|
-
else {
|
|
2290
|
-
applyWhenToken(internal, token, whenCode);
|
|
2291
|
-
continue;
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
// Day of week
|
|
2295
|
-
const rangeConsumed = tryConsumeDayRangeTokens(internal, tokens, i);
|
|
2296
|
-
if (rangeConsumed > 0) {
|
|
2297
|
-
continue;
|
|
2298
|
-
}
|
|
2299
|
-
const days = resolveDayTokenDays(normalizeTokenLower(token));
|
|
2300
|
-
if (days) {
|
|
2301
|
-
addDayOfWeekList(internal, days);
|
|
2302
|
-
mark(internal.consumed, token);
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
// Units following numbers handled later
|
|
2306
|
-
if (tryRouteSynonym(i)) {
|
|
2307
|
-
continue;
|
|
2308
|
-
}
|
|
2309
|
-
if (eyeSite && treatEyeTokenAsSite) {
|
|
2310
|
-
internal.siteText = eyeSite.site;
|
|
2311
|
-
internal.siteSource = "abbreviation";
|
|
2312
|
-
if (eyeSite.route && !internal.routeCode) {
|
|
2313
|
-
setRoute(internal, eyeSite.route);
|
|
2314
|
-
}
|
|
2315
|
-
mark(internal.consumed, token);
|
|
2316
|
-
continue;
|
|
2317
|
-
}
|
|
2318
|
-
if (internal.count === undefined) {
|
|
2319
|
-
const countMatch = token.lower.match(/^[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
2320
|
-
if (countMatch) {
|
|
2321
|
-
if (applyCountLimit(internal, parseFloat(countMatch[1]))) {
|
|
2322
|
-
mark(internal.consumed, token);
|
|
2323
|
-
const nextToken = tokens[i + 1];
|
|
2324
|
-
if (nextToken && COUNT_KEYWORDS.has(nextToken.lower)) {
|
|
2325
|
-
mark(internal.consumed, nextToken);
|
|
2326
|
-
}
|
|
2327
|
-
continue;
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
if (token.lower === "x" || token.lower === "*") {
|
|
2331
|
-
const numericToken = tokens[i + 1];
|
|
2332
|
-
if (numericToken &&
|
|
2333
|
-
!internal.consumed.has(numericToken.index) &&
|
|
2334
|
-
/^[0-9]+(?:\.[0-9]+)?$/.test(numericToken.lower) &&
|
|
2335
|
-
applyCountLimit(internal, parseFloat(numericToken.original))) {
|
|
2336
|
-
mark(internal.consumed, token);
|
|
2337
|
-
mark(internal.consumed, numericToken);
|
|
2338
|
-
const afterToken = tokens[i + 2];
|
|
2339
|
-
if (afterToken && COUNT_KEYWORDS.has(afterToken.lower)) {
|
|
2340
|
-
mark(internal.consumed, afterToken);
|
|
2341
|
-
}
|
|
2342
|
-
continue;
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
if (token.lower === "for") {
|
|
2346
|
-
const skipConnectors = (startIndex, bucket) => {
|
|
2347
|
-
let cursor = startIndex;
|
|
2348
|
-
while (cursor < tokens.length) {
|
|
2349
|
-
const candidate = tokens[cursor];
|
|
2350
|
-
if (!candidate) {
|
|
2351
|
-
break;
|
|
2352
|
-
}
|
|
2353
|
-
if (internal.consumed.has(candidate.index)) {
|
|
2354
|
-
cursor += 1;
|
|
2355
|
-
continue;
|
|
2356
|
-
}
|
|
2357
|
-
if (!COUNT_CONNECTOR_WORDS.has(candidate.lower)) {
|
|
2358
|
-
break;
|
|
2359
|
-
}
|
|
2360
|
-
bucket.push(candidate);
|
|
2361
|
-
cursor += 1;
|
|
2362
|
-
}
|
|
2363
|
-
return cursor;
|
|
2364
|
-
};
|
|
2365
|
-
const preConnectors = [];
|
|
2366
|
-
let lookaheadIndex = skipConnectors(i + 1, preConnectors);
|
|
2367
|
-
const numericToken = tokens[lookaheadIndex];
|
|
2368
|
-
if (numericToken &&
|
|
2369
|
-
!internal.consumed.has(numericToken.index) &&
|
|
2370
|
-
/^[0-9]+(?:\.[0-9]+)?$/.test(numericToken.lower)) {
|
|
2371
|
-
const postConnectors = [];
|
|
2372
|
-
lookaheadIndex = skipConnectors(lookaheadIndex + 1, postConnectors);
|
|
2373
|
-
const keywordToken = tokens[lookaheadIndex];
|
|
2374
|
-
if (keywordToken &&
|
|
2375
|
-
!internal.consumed.has(keywordToken.index) &&
|
|
2376
|
-
COUNT_KEYWORDS.has(keywordToken.lower) &&
|
|
2377
|
-
applyCountLimit(internal, parseFloat(numericToken.original))) {
|
|
2378
|
-
mark(internal.consumed, token);
|
|
2379
|
-
for (const connector of preConnectors) {
|
|
2380
|
-
mark(internal.consumed, connector);
|
|
2381
|
-
}
|
|
2382
|
-
mark(internal.consumed, numericToken);
|
|
2383
|
-
for (const connector of postConnectors) {
|
|
2384
|
-
mark(internal.consumed, connector);
|
|
2385
|
-
}
|
|
2386
|
-
mark(internal.consumed, keywordToken);
|
|
2387
|
-
continue;
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
if (COUNT_KEYWORDS.has(token.lower)) {
|
|
2392
|
-
const partsToMark = [token];
|
|
2393
|
-
let value;
|
|
2394
|
-
const prevToken = tokens[i - 1];
|
|
2395
|
-
if (prevToken && !internal.consumed.has(prevToken.index)) {
|
|
2396
|
-
const prevLower = prevToken.lower;
|
|
2397
|
-
const suffixMatch = prevLower.match(/^([0-9]+(?:\.[0-9]+)?)[x*]$/);
|
|
2398
|
-
const prefixMatch = prevLower.match(/^[x*]([0-9]+(?:\.[0-9]+)?)$/);
|
|
2399
|
-
if (suffixMatch) {
|
|
2400
|
-
value = parseFloat(suffixMatch[1]);
|
|
2401
|
-
partsToMark.push(prevToken);
|
|
2402
|
-
}
|
|
2403
|
-
else if (prefixMatch) {
|
|
2404
|
-
value = parseFloat(prefixMatch[1]);
|
|
2405
|
-
partsToMark.push(prevToken);
|
|
2406
|
-
}
|
|
2407
|
-
else if (/^[0-9]+(?:\.[0-9]+)?$/.test(prevLower)) {
|
|
2408
|
-
const maybeX = tokens[i - 2];
|
|
2409
|
-
if (maybeX &&
|
|
2410
|
-
!internal.consumed.has(maybeX.index) &&
|
|
2411
|
-
(maybeX.lower === "x" || maybeX.lower === "*")) {
|
|
2412
|
-
value = parseFloat(prevToken.original);
|
|
2413
|
-
partsToMark.push(maybeX, prevToken);
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
if (value === undefined) {
|
|
2418
|
-
const nextToken = tokens[i + 1];
|
|
2419
|
-
if (nextToken &&
|
|
2420
|
-
!internal.consumed.has(nextToken.index) &&
|
|
2421
|
-
/^[0-9]+(?:\.[0-9]+)?$/.test(nextToken.lower)) {
|
|
2422
|
-
value = parseFloat(nextToken.original);
|
|
2423
|
-
partsToMark.push(nextToken);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
if (applyCountLimit(internal, value)) {
|
|
2427
|
-
for (const part of partsToMark) {
|
|
2428
|
-
mark(internal.consumed, part);
|
|
2429
|
-
}
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
// Numeric dose
|
|
2435
|
-
if (tryParseCountBasedFrequency(internal, tokens, i, options)) {
|
|
2436
|
-
continue;
|
|
2437
|
-
}
|
|
2438
|
-
const rangeValue = parseNumericRange(token.lower);
|
|
2439
|
-
if (rangeValue) {
|
|
2440
|
-
if (!internal.doseRange) {
|
|
2441
|
-
internal.doseRange = rangeValue;
|
|
2442
|
-
}
|
|
2443
|
-
mark(internal.consumed, token);
|
|
2444
|
-
const resolvedUnit = resolveUnitTokenAt(tokens, i + 1, internal.consumed, options);
|
|
2445
|
-
if (resolvedUnit) {
|
|
2446
|
-
internal.unit = resolvedUnit.unit;
|
|
2447
|
-
for (const consumedIndex of resolvedUnit.consumedIndices) {
|
|
2448
|
-
mark(internal.consumed, tokens[consumedIndex]);
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
continue;
|
|
2452
|
-
}
|
|
2453
|
-
if (/^[0-9]+(?:\.[0-9]+)?$/.test(token.lower)) {
|
|
2454
|
-
const value = parseFloat(token.original);
|
|
2455
|
-
const resolvedDose = resolveNumericDoseUnit(tokens, i, value, internal.consumed, options);
|
|
2456
|
-
if (internal.dose === undefined) {
|
|
2457
|
-
internal.dose = resolvedDose.doseValue;
|
|
2458
|
-
}
|
|
2459
|
-
mark(internal.consumed, token);
|
|
2460
|
-
if (resolvedDose.unit) {
|
|
2461
|
-
internal.unit = resolvedDose.unit;
|
|
2462
|
-
}
|
|
2463
|
-
for (const consumedIndex of resolvedDose.consumedIndices) {
|
|
2464
|
-
mark(internal.consumed, tokens[consumedIndex]);
|
|
2465
|
-
}
|
|
2466
|
-
continue;
|
|
2467
|
-
}
|
|
2468
|
-
// Patterns like 1x or 2x
|
|
2469
|
-
const timesMatch = token.lower.match(/^([0-9]+(?:\.[0-9]+)?)[x*]$/);
|
|
2470
|
-
if (timesMatch) {
|
|
2471
|
-
const val = parseFloat(timesMatch[1]);
|
|
2472
|
-
if (internal.dose === undefined) {
|
|
2473
|
-
internal.dose = val;
|
|
2474
|
-
}
|
|
2475
|
-
mark(internal.consumed, token);
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
// Words for frequency
|
|
2479
|
-
const wordFreq = maps_1.WORD_FREQUENCIES[token.lower];
|
|
2480
|
-
if (wordFreq) {
|
|
2481
|
-
internal.frequency = wordFreq.frequency;
|
|
2482
|
-
internal.period = 1;
|
|
2483
|
-
internal.periodUnit = wordFreq.periodUnit;
|
|
2484
|
-
mark(internal.consumed, token);
|
|
2485
|
-
continue;
|
|
2486
|
-
}
|
|
2487
|
-
// Skip generic connectors
|
|
2488
|
-
if (token.lower === "per" || token.lower === "a" || token.lower === "every" || token.lower === "each") {
|
|
2489
|
-
mark(internal.consumed, token);
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
// Units from trailing tokens if still undefined
|
|
2494
|
-
if (internal.unit === undefined) {
|
|
2495
|
-
for (const token of tokens) {
|
|
2496
|
-
if (internal.consumed.has(token.index))
|
|
2497
|
-
continue;
|
|
2498
|
-
const unit = normalizeUnit(token.lower, options);
|
|
2499
|
-
if (unit) {
|
|
2500
|
-
internal.unit = unit;
|
|
2501
|
-
mark(internal.consumed, token);
|
|
2502
|
-
break;
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
if (internal.unit === undefined) {
|
|
2507
|
-
internal.unit = enforceHouseholdUnitPolicy((0, context_1.inferUnitFromContext)(context), options);
|
|
2508
|
-
}
|
|
2509
|
-
if (internal.unit === undefined) {
|
|
2510
|
-
const fallbackUnit = enforceHouseholdUnitPolicy(inferUnitFromRouteHints(internal), options);
|
|
2511
|
-
if (fallbackUnit) {
|
|
2512
|
-
internal.unit = fallbackUnit;
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
if ((options === null || options === void 0 ? void 0 : options.assumeSingleDiscreteDose) &&
|
|
2516
|
-
internal.dose === undefined &&
|
|
2517
|
-
internal.doseRange === undefined &&
|
|
2518
|
-
internal.unit !== undefined &&
|
|
2519
|
-
isDiscreteUnit(internal.unit)) {
|
|
2520
|
-
internal.dose = 1;
|
|
2521
|
-
}
|
|
2522
|
-
// Frequency defaults when timing code implies it
|
|
2523
|
-
if (internal.frequency === undefined &&
|
|
2524
|
-
internal.period === undefined &&
|
|
2525
|
-
internal.timingCode) {
|
|
2526
|
-
const descriptor = maps_1.TIMING_ABBREVIATIONS[internal.timingCode.toLowerCase()];
|
|
2527
|
-
if (descriptor) {
|
|
2528
|
-
if (descriptor.frequency !== undefined) {
|
|
2529
|
-
internal.frequency = descriptor.frequency;
|
|
2530
|
-
}
|
|
2531
|
-
if (descriptor.period !== undefined) {
|
|
2532
|
-
internal.period = descriptor.period;
|
|
2533
|
-
}
|
|
2534
|
-
if (descriptor.periodUnit) {
|
|
2535
|
-
internal.periodUnit = descriptor.periodUnit;
|
|
2536
|
-
}
|
|
2537
|
-
if (descriptor.when) {
|
|
2538
|
-
for (const w of descriptor.when) {
|
|
2539
|
-
addWhen(internal.when, w);
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
if (!internal.timingCode &&
|
|
2545
|
-
internal.frequency !== undefined &&
|
|
2546
|
-
internal.periodUnit === types_1.FhirPeriodUnit.Day &&
|
|
2547
|
-
(internal.period === undefined || internal.period === 1)) {
|
|
2548
|
-
if (internal.frequency === 2) {
|
|
2549
|
-
internal.timingCode = "BID";
|
|
2550
|
-
}
|
|
2551
|
-
else if (internal.frequency === 3) {
|
|
2552
|
-
internal.timingCode = "TID";
|
|
2553
|
-
}
|
|
2554
|
-
else if (internal.frequency === 4) {
|
|
2555
|
-
internal.timingCode = "QID";
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
|
-
reconcileMealTimingSpecificity(internal);
|
|
2559
|
-
// Expand generic meal markers into specific EventTiming codes when asked to.
|
|
2560
|
-
expandMealTimings(internal, options);
|
|
2561
|
-
sortWhenValues(internal, options);
|
|
2562
|
-
// PRN reason text
|
|
2563
|
-
if (internal.asNeeded && prnReasonStart !== undefined) {
|
|
2564
|
-
const reasonTokens = [];
|
|
2565
|
-
const reasonIndices = [];
|
|
2566
|
-
const reasonObjects = [];
|
|
2567
|
-
const PRN_RECLAIMABLE_CONNECTORS = new Set(["at", "to", "in", "into", "on", "onto"]);
|
|
2568
|
-
for (let i = prnReasonStart; i < tokens.length; i++) {
|
|
2569
|
-
const token = tokens[i];
|
|
2570
|
-
if (internal.consumed.has(token.index)) {
|
|
2571
|
-
// We only allow reclaiming certain generic connectors if they were used
|
|
2572
|
-
// as standalone markers (like 'at' or 'to') and not if they were clearly
|
|
2573
|
-
// part of a frequency/period instruction (which would be skipped here
|
|
2574
|
-
// if they were consumed by those specific logic paths).
|
|
2575
|
-
if (!PRN_RECLAIMABLE_CONNECTORS.has(token.lower)) {
|
|
2576
|
-
continue;
|
|
2577
|
-
}
|
|
2578
|
-
// If it is a reclaimable connector, we can pull it back into the reason
|
|
2579
|
-
// if it helps form a coherent phrase like 'irritation at rectum'.
|
|
2580
|
-
}
|
|
2581
|
-
// If we haven't started collecting the reason yet, we should skip introductory
|
|
2582
|
-
// connectors to avoid phrases like "as needed for if pain".
|
|
2583
|
-
const PRN_INTRODUCTIONS = new Set(["for", "if", "when", "upon", "due", "to"]);
|
|
2584
|
-
if (reasonTokens.length === 0 && PRN_INTRODUCTIONS.has(token.lower)) {
|
|
2585
|
-
// Special handling for "due to" - if we skipped "due", we should also skip "to"
|
|
2586
|
-
if (token.lower === "due") {
|
|
2587
|
-
const next = tokens[i + 1];
|
|
2588
|
-
if (next && next.lower === "to") {
|
|
2589
|
-
mark(internal.consumed, token);
|
|
2590
|
-
mark(internal.consumed, next);
|
|
2591
|
-
i++; // skip next token in loop
|
|
2592
|
-
continue;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
mark(internal.consumed, token);
|
|
2596
|
-
continue;
|
|
2597
|
-
}
|
|
2598
|
-
reasonTokens.push(token.original);
|
|
2599
|
-
reasonIndices.push(token.index);
|
|
2600
|
-
reasonObjects.push(token);
|
|
2601
|
-
mark(internal.consumed, token);
|
|
2602
|
-
}
|
|
2603
|
-
if (reasonTokens.length > 0) {
|
|
2604
|
-
let sortedIndices = reasonIndices.slice().sort((a, b) => a - b);
|
|
2605
|
-
let range = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
2606
|
-
let sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
2607
|
-
if (sourceText) {
|
|
2608
|
-
const cutoff = determinePrnReasonCutoff(reasonObjects, sourceText);
|
|
2609
|
-
if (cutoff !== undefined) {
|
|
2610
|
-
for (let i = cutoff; i < reasonObjects.length; i++) {
|
|
2611
|
-
internal.consumed.delete(reasonObjects[i].index);
|
|
2612
|
-
}
|
|
2613
|
-
reasonObjects.splice(cutoff);
|
|
2614
|
-
reasonTokens.splice(cutoff);
|
|
2615
|
-
reasonIndices.splice(cutoff);
|
|
2616
|
-
while (reasonTokens.length > 0) {
|
|
2617
|
-
const lastToken = reasonTokens[reasonTokens.length - 1];
|
|
2618
|
-
if (!lastToken || /^[;:.,-]+$/.test(lastToken.trim())) {
|
|
2619
|
-
const removedObject = reasonObjects.pop();
|
|
2620
|
-
if (removedObject) {
|
|
2621
|
-
internal.consumed.delete(removedObject.index);
|
|
2622
|
-
}
|
|
2623
|
-
reasonTokens.pop();
|
|
2624
|
-
const removedIndex = reasonIndices.pop();
|
|
2625
|
-
if (removedIndex !== undefined) {
|
|
2626
|
-
internal.consumed.delete(removedIndex);
|
|
2627
|
-
}
|
|
2628
|
-
continue;
|
|
2629
|
-
}
|
|
2630
|
-
break;
|
|
2631
|
-
}
|
|
2632
|
-
if (reasonTokens.length > 0) {
|
|
2633
|
-
sortedIndices = reasonIndices.slice().sort((a, b) => a - b);
|
|
2634
|
-
range = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
2635
|
-
sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
2636
|
-
}
|
|
2637
|
-
else {
|
|
2638
|
-
range = undefined;
|
|
2639
|
-
sourceText = undefined;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
let canonicalPrefix;
|
|
2644
|
-
if (reasonTokens.length > 0) {
|
|
2645
|
-
const suffixInfo = findTrailingPrnSiteSuffix(reasonObjects, internal, options);
|
|
2646
|
-
if ((_h = suffixInfo === null || suffixInfo === void 0 ? void 0 : suffixInfo.tokens) === null || _h === void 0 ? void 0 : _h.length) {
|
|
2647
|
-
for (const token of suffixInfo.tokens) {
|
|
2648
|
-
prnSiteSuffixIndices.add(token.index);
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
if (suffixInfo && suffixInfo.startIndex > 0) {
|
|
2652
|
-
const prefixTokens = reasonObjects
|
|
2653
|
-
.slice(0, suffixInfo.startIndex)
|
|
2654
|
-
.map((token) => token.original)
|
|
2655
|
-
.join(" ")
|
|
2656
|
-
.replace(/\s+/g, " ")
|
|
2657
|
-
.trim();
|
|
2658
|
-
if (prefixTokens) {
|
|
2659
|
-
canonicalPrefix = prefixTokens.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
}
|
|
2663
|
-
if (reasonTokens.length > 0) {
|
|
2664
|
-
const joined = reasonTokens.join(" ").trim();
|
|
2665
|
-
if (joined) {
|
|
2666
|
-
let sanitized = joined.replace(/\s+/g, " ").trim();
|
|
2667
|
-
let isProbe = false;
|
|
2668
|
-
const probeMatch = sanitized.match(/^\{(.+)}$/);
|
|
2669
|
-
if (probeMatch) {
|
|
2670
|
-
isProbe = true;
|
|
2671
|
-
sanitized = probeMatch[1];
|
|
2672
|
-
}
|
|
2673
|
-
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
2674
|
-
const text = sanitized || joined;
|
|
2675
|
-
internal.asNeededReason = text;
|
|
2676
|
-
const normalized = text.toLowerCase();
|
|
2677
|
-
const canonicalSource = canonicalPrefix || sanitized || text;
|
|
2678
|
-
const canonical = canonicalSource
|
|
2679
|
-
? (0, maps_1.normalizePrnReasonKey)(canonicalSource)
|
|
2680
|
-
: (0, maps_1.normalizePrnReasonKey)(text);
|
|
2681
|
-
internal.prnReasonLookupRequest = {
|
|
2682
|
-
originalText: joined,
|
|
2683
|
-
text,
|
|
2684
|
-
normalized,
|
|
2685
|
-
canonical: canonical !== null && canonical !== void 0 ? canonical : "",
|
|
2686
|
-
isProbe,
|
|
2687
|
-
inputText: internal.input,
|
|
2688
|
-
sourceText,
|
|
2689
|
-
range
|
|
2690
|
-
};
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
collectAdditionalInstructions(internal, tokens);
|
|
2696
|
-
// Determine site text from leftover tokens (excluding PRN reason tokens)
|
|
2697
|
-
const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
|
|
2698
|
-
const siteCandidateIndices = new Set();
|
|
2699
|
-
const leftoverSiteIndices = new Set();
|
|
2700
|
-
for (const token of leftoverTokens) {
|
|
2701
|
-
if (prnSiteSuffixIndices.has(token.index)) {
|
|
2702
|
-
continue;
|
|
2703
|
-
}
|
|
2704
|
-
const normalized = normalizeTokenLower(token);
|
|
2705
|
-
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
2706
|
-
siteCandidateIndices.add(token.index);
|
|
2707
|
-
leftoverSiteIndices.add(token.index);
|
|
2708
|
-
continue;
|
|
2709
|
-
}
|
|
2710
|
-
if (SITE_CONNECTORS.has(normalized)) {
|
|
2711
|
-
const next = tokens[token.index + 1];
|
|
2712
|
-
if (next && !internal.consumed.has(next.index) && !prnSiteSuffixIndices.has(next.index)) {
|
|
2713
|
-
siteCandidateIndices.add(next.index);
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
if (leftoverSiteIndices.size === 0) {
|
|
2718
|
-
for (const idx of internal.siteTokenIndices) {
|
|
2719
|
-
if (prnSiteSuffixIndices.has(idx)) {
|
|
2720
|
-
continue;
|
|
2721
|
-
}
|
|
2722
|
-
siteCandidateIndices.add(idx);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
if (siteCandidateIndices.size > 0) {
|
|
2726
|
-
const indicesToInclude = new Set(siteCandidateIndices);
|
|
2727
|
-
for (const idx of siteCandidateIndices) {
|
|
2728
|
-
let prev = idx - 1;
|
|
2729
|
-
while (prev >= 0) {
|
|
2730
|
-
const token = tokens[prev];
|
|
2731
|
-
if (!token) {
|
|
2732
|
-
break;
|
|
2733
|
-
}
|
|
2734
|
-
const lower = normalizeTokenLower(token);
|
|
2735
|
-
if (SITE_CONNECTORS.has(lower) ||
|
|
2736
|
-
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
2737
|
-
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
2738
|
-
indicesToInclude.add(token.index);
|
|
2739
|
-
prev -= 1;
|
|
2740
|
-
continue;
|
|
2741
|
-
}
|
|
2742
|
-
break;
|
|
2743
|
-
}
|
|
2744
|
-
let next = idx + 1;
|
|
2745
|
-
while (next < tokens.length) {
|
|
2746
|
-
const token = tokens[next];
|
|
2747
|
-
if (!token) {
|
|
2748
|
-
break;
|
|
2749
|
-
}
|
|
2750
|
-
const lower = normalizeTokenLower(token);
|
|
2751
|
-
if (SITE_CONNECTORS.has(lower) ||
|
|
2752
|
-
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
2753
|
-
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
2754
|
-
indicesToInclude.add(token.index);
|
|
2755
|
-
next += 1;
|
|
2756
|
-
continue;
|
|
2757
|
-
}
|
|
2758
|
-
break;
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
const sortedIndices = Array.from(indicesToInclude).sort((a, b) => a - b);
|
|
2762
|
-
const displayWords = [];
|
|
2763
|
-
for (const index of sortedIndices) {
|
|
2764
|
-
const token = tokens[index];
|
|
2765
|
-
if (!token) {
|
|
2766
|
-
continue;
|
|
2767
|
-
}
|
|
2768
|
-
const lower = normalizeTokenLower(token);
|
|
2769
|
-
const trimmed = token.original.trim();
|
|
2770
|
-
const isBraceToken = trimmed.length > 0 && /^[{}]+$/.test(trimmed);
|
|
2771
|
-
if (!isBraceToken && !SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
|
|
2772
|
-
displayWords.push(token.original);
|
|
2773
|
-
}
|
|
2774
|
-
mark(internal.consumed, token);
|
|
2775
|
-
}
|
|
2776
|
-
const normalizedSite = displayWords
|
|
2777
|
-
.filter((word) => !SITE_CONNECTORS.has(word.trim().toLowerCase()))
|
|
2778
|
-
.join(" ")
|
|
2779
|
-
.trim();
|
|
2780
|
-
if (normalizedSite) {
|
|
2781
|
-
const tokenRange = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
2782
|
-
let sanitized = normalizedSite;
|
|
2783
|
-
let isProbe = false;
|
|
2784
|
-
const probeMatch = sanitized.match(/^\{(.+)}$/);
|
|
2785
|
-
if (probeMatch) {
|
|
2786
|
-
// `{site}` placeholders flag interactive lookups so consumers can prompt
|
|
2787
|
-
// for a coded selection even when the parser cannot resolve the entry.
|
|
2788
|
-
isProbe = true;
|
|
2789
|
-
sanitized = probeMatch[1];
|
|
2790
|
-
}
|
|
2791
|
-
// Remove stray braces and normalize whitespace so lookups and downstream
|
|
2792
|
-
// displays operate on a clean phrase.
|
|
2793
|
-
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
2794
|
-
const range = refineSiteRange(internal.input, sanitized, tokenRange);
|
|
2795
|
-
const sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
2796
|
-
const displayText = normalizeSiteDisplayText(sanitized, options === null || options === void 0 ? void 0 : options.siteCodeMap);
|
|
2797
|
-
const displayLower = displayText.toLowerCase();
|
|
2798
|
-
const canonical = displayText ? (0, maps_1.normalizeBodySiteKey)(displayText) : "";
|
|
2799
|
-
internal.siteLookupRequest = {
|
|
2800
|
-
originalText: normalizedSite,
|
|
2801
|
-
text: displayText,
|
|
2802
|
-
normalized: displayLower,
|
|
2803
|
-
canonical,
|
|
2804
|
-
isProbe,
|
|
2805
|
-
inputText: internal.input,
|
|
2806
|
-
sourceText,
|
|
2807
|
-
range
|
|
2808
|
-
};
|
|
2809
|
-
if (displayText) {
|
|
2810
|
-
const normalizedLower = sanitized.toLowerCase();
|
|
2811
|
-
const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
|
|
2812
|
-
const siteWords = displayLower.split(/\s+/).filter((word) => word.length > 0);
|
|
2813
|
-
const hasNonSiteWords = siteWords.some((word) => !isBodySiteHint(word, internal.customSiteHints));
|
|
2814
|
-
const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
|
|
2815
|
-
const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
|
|
2816
|
-
if (!appliedRouteDescriptor) {
|
|
2817
|
-
// Preserve the clean site text for FHIR output and resolver context
|
|
2818
|
-
// whenever we keep the original phrase.
|
|
2819
|
-
internal.siteText = displayText;
|
|
2820
|
-
if (!internal.siteSource) {
|
|
2821
|
-
internal.siteSource = "text";
|
|
2822
|
-
}
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
if (!internal.routeCode && internal.siteText) {
|
|
2828
|
-
for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
|
|
2829
|
-
if (pattern.test(internal.siteText)) {
|
|
2830
|
-
setRoute(internal, route);
|
|
2831
|
-
break;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
if (internal.routeCode === types_1.RouteCode["Intravitreal route (qualifier value)"] &&
|
|
2836
|
-
(!internal.siteText || !/eye/i.test(internal.siteText))) {
|
|
2837
|
-
internal.warnings.push("Intravitreal administrations require an eye site (e.g., OD/OS/OU).");
|
|
2838
|
-
}
|
|
2839
|
-
return internal;
|
|
2840
|
-
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Resolves parsed site text against SNOMED dictionaries and synchronous
|
|
2843
|
-
* callbacks, applying the best match to the in-progress parse result.
|
|
2844
|
-
*/
|
|
2845
|
-
function applyPrnReasonCoding(internal, options) {
|
|
2846
|
-
runPrnReasonResolutionSync(internal, options);
|
|
2847
|
-
}
|
|
2848
|
-
function applyPrnReasonCodingAsync(internal, options) {
|
|
2849
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2850
|
-
yield runPrnReasonResolutionAsync(internal, options);
|
|
2851
|
-
});
|
|
2852
|
-
}
|
|
2853
|
-
function applySiteCoding(internal, options) {
|
|
2854
|
-
runSiteCodingResolutionSync(internal, options);
|
|
2855
|
-
}
|
|
2856
|
-
/**
|
|
2857
|
-
* Asynchronous counterpart to {@link applySiteCoding} that awaits resolver and
|
|
2858
|
-
* suggestion callbacks so remote terminology services can be used.
|
|
2859
|
-
*/
|
|
2860
|
-
function applySiteCodingAsync(internal, options) {
|
|
2861
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2862
|
-
yield runSiteCodingResolutionAsync(internal, options);
|
|
2863
|
-
});
|
|
2864
|
-
}
|
|
2865
|
-
/**
|
|
2866
|
-
* Attempts to resolve site codings using built-in dictionaries followed by any
|
|
2867
|
-
* provided synchronous resolvers. Suggestions are collected when resolution
|
|
2868
|
-
* fails or a `{probe}` placeholder requested an interactive lookup.
|
|
2869
|
-
*/
|
|
2870
|
-
function runSiteCodingResolutionSync(internal, options) {
|
|
2871
|
-
internal.siteLookups = [];
|
|
2872
|
-
const request = internal.siteLookupRequest;
|
|
2873
|
-
if (!request) {
|
|
2874
|
-
return;
|
|
2875
|
-
}
|
|
2876
|
-
const canonical = request.canonical;
|
|
2877
|
-
const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
|
|
2878
|
-
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
2879
|
-
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
2880
|
-
if (!resolution) {
|
|
2881
|
-
// Allow synchronous resolver callbacks to claim the site.
|
|
2882
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
2883
|
-
const result = resolver(request);
|
|
2884
|
-
if (isPromise(result)) {
|
|
2885
|
-
throw new Error("Site code resolver returned a Promise; use parseSigAsync for asynchronous site resolution.");
|
|
2886
|
-
}
|
|
2887
|
-
if (result) {
|
|
2888
|
-
resolution = result;
|
|
2889
|
-
break;
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
}
|
|
2893
|
-
const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
|
|
2894
|
-
if (!resolution && defaultDefinition) {
|
|
2895
|
-
// Fall back to bundled SNOMED lookups when no overrides claim the site.
|
|
2896
|
-
resolution = defaultDefinition;
|
|
2897
|
-
}
|
|
2898
|
-
if (resolution) {
|
|
2899
|
-
applySiteDefinition(internal, resolution);
|
|
2900
|
-
}
|
|
2901
|
-
else {
|
|
2902
|
-
internal.siteCoding = undefined;
|
|
2903
|
-
}
|
|
2904
|
-
const needsSuggestions = request.isProbe || !resolution;
|
|
2905
|
-
if (!needsSuggestions) {
|
|
2906
|
-
return;
|
|
2907
|
-
}
|
|
2908
|
-
const suggestionMap = new Map();
|
|
2909
|
-
if (selection) {
|
|
2910
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
|
|
2911
|
-
}
|
|
2912
|
-
if (customDefinition) {
|
|
2913
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
2914
|
-
}
|
|
2915
|
-
if (defaultDefinition) {
|
|
2916
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
|
|
2917
|
-
}
|
|
2918
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
|
|
2919
|
-
// Aggregates resolver suggestions while guarding against accidental async
|
|
2920
|
-
// usage, mirroring the behavior of site resolvers.
|
|
2921
|
-
const result = resolver(request);
|
|
2922
|
-
if (isPromise(result)) {
|
|
2923
|
-
throw new Error("Site code suggestion resolver returned a Promise; use parseSigAsync for asynchronous site suggestions.");
|
|
2924
|
-
}
|
|
2925
|
-
collectSuggestionResult(suggestionMap, result);
|
|
2926
|
-
}
|
|
2927
|
-
const suggestions = Array.from(suggestionMap.values());
|
|
2928
|
-
if (suggestions.length || request.isProbe) {
|
|
2929
|
-
internal.siteLookups.push({ request, suggestions });
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
/**
|
|
2933
|
-
* Async version of {@link runSiteCodingResolutionSync} that awaits resolver
|
|
2934
|
-
* results and suggestion providers, enabling remote terminology services.
|
|
2935
|
-
*/
|
|
2936
|
-
function runSiteCodingResolutionAsync(internal, options) {
|
|
2937
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2938
|
-
internal.siteLookups = [];
|
|
2939
|
-
const request = internal.siteLookupRequest;
|
|
2940
|
-
if (!request) {
|
|
2941
|
-
return;
|
|
2942
|
-
}
|
|
2943
|
-
const canonical = request.canonical;
|
|
2944
|
-
const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
|
|
2945
|
-
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
2946
|
-
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
2947
|
-
if (!resolution) {
|
|
2948
|
-
// Await asynchronous resolver callbacks (e.g., HTTP terminology services).
|
|
2949
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
2950
|
-
const result = yield resolver(request);
|
|
2951
|
-
if (result) {
|
|
2952
|
-
resolution = result;
|
|
2953
|
-
break;
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
|
|
2958
|
-
if (!resolution && defaultDefinition) {
|
|
2959
|
-
resolution = defaultDefinition;
|
|
2960
|
-
}
|
|
2961
|
-
if (resolution) {
|
|
2962
|
-
applySiteDefinition(internal, resolution);
|
|
2963
|
-
}
|
|
2964
|
-
else {
|
|
2965
|
-
internal.siteCoding = undefined;
|
|
2966
|
-
}
|
|
2967
|
-
const needsSuggestions = request.isProbe || !resolution;
|
|
2968
|
-
if (!needsSuggestions) {
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
const suggestionMap = new Map();
|
|
2972
|
-
if (selection) {
|
|
2973
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
|
|
2974
|
-
}
|
|
2975
|
-
if (customDefinition) {
|
|
2976
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
2977
|
-
}
|
|
2978
|
-
if (defaultDefinition) {
|
|
2979
|
-
addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
|
|
2980
|
-
}
|
|
2981
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
|
|
2982
|
-
// Async suggestion providers are awaited, allowing UI workflows to fetch
|
|
2983
|
-
// candidate codes on demand.
|
|
2984
|
-
const result = yield resolver(request);
|
|
2985
|
-
collectSuggestionResult(suggestionMap, result);
|
|
2986
|
-
}
|
|
2987
|
-
const suggestions = Array.from(suggestionMap.values());
|
|
2988
|
-
if (suggestions.length || request.isProbe) {
|
|
2989
|
-
internal.siteLookups.push({ request, suggestions });
|
|
2990
|
-
}
|
|
2991
|
-
});
|
|
2992
|
-
}
|
|
2993
|
-
/**
|
|
2994
|
-
* Looks up a body-site definition in a caller-provided map, honoring both
|
|
2995
|
-
* direct keys and entries that normalize to the same canonical phrase.
|
|
2996
|
-
*/
|
|
2997
|
-
function lookupBodySiteDefinition(map, canonical) {
|
|
2998
|
-
if (!map) {
|
|
2999
|
-
return undefined;
|
|
3000
|
-
}
|
|
3001
|
-
const direct = map[canonical];
|
|
3002
|
-
if (direct) {
|
|
3003
|
-
return direct;
|
|
3004
|
-
}
|
|
3005
|
-
for (const [key, definition] of (0, object_1.objectEntries)(map)) {
|
|
3006
|
-
if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
|
|
3007
|
-
return definition;
|
|
3008
|
-
}
|
|
3009
|
-
if (definition.aliases) {
|
|
3010
|
-
for (const alias of definition.aliases) {
|
|
3011
|
-
if ((0, maps_1.normalizeBodySiteKey)(alias) === canonical) {
|
|
3012
|
-
return definition;
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
return undefined;
|
|
3018
|
-
}
|
|
3019
|
-
function pickSiteSelection(selections, request) {
|
|
3020
|
-
if (!selections) {
|
|
3021
|
-
return undefined;
|
|
3022
|
-
}
|
|
3023
|
-
const canonical = request.canonical;
|
|
3024
|
-
const normalizedText = (0, maps_1.normalizeBodySiteKey)(request.text);
|
|
3025
|
-
const requestRange = request.range;
|
|
3026
|
-
for (const selection of toArray(selections)) {
|
|
3027
|
-
if (!selection) {
|
|
3028
|
-
continue;
|
|
3029
|
-
}
|
|
3030
|
-
let matched = false;
|
|
3031
|
-
if (selection.range) {
|
|
3032
|
-
if (!requestRange) {
|
|
3033
|
-
continue;
|
|
3034
|
-
}
|
|
3035
|
-
if (selection.range.start !== requestRange.start ||
|
|
3036
|
-
selection.range.end !== requestRange.end) {
|
|
3037
|
-
continue;
|
|
3038
|
-
}
|
|
3039
|
-
matched = true;
|
|
3040
|
-
}
|
|
3041
|
-
if (selection.canonical) {
|
|
3042
|
-
if ((0, maps_1.normalizeBodySiteKey)(selection.canonical) !== canonical) {
|
|
3043
|
-
continue;
|
|
3044
|
-
}
|
|
3045
|
-
matched = true;
|
|
3046
|
-
}
|
|
3047
|
-
else if (selection.text) {
|
|
3048
|
-
const normalizedSelection = (0, maps_1.normalizeBodySiteKey)(selection.text);
|
|
3049
|
-
if (normalizedSelection !== canonical && normalizedSelection !== normalizedText) {
|
|
3050
|
-
continue;
|
|
3051
|
-
}
|
|
3052
|
-
matched = true;
|
|
3053
|
-
}
|
|
3054
|
-
if (!selection.range && !selection.canonical && !selection.text) {
|
|
3055
|
-
continue;
|
|
3056
|
-
}
|
|
3057
|
-
if (matched) {
|
|
3058
|
-
return selection.resolution;
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
return undefined;
|
|
3062
|
-
}
|
|
3063
|
-
/**
|
|
3064
|
-
* Applies the selected body-site definition onto the parser state, defaulting
|
|
3065
|
-
* the coding system to SNOMED CT when the definition omits one.
|
|
3066
|
-
*/
|
|
3067
|
-
function applySiteDefinition(internal, definition) {
|
|
3068
|
-
var _a, _b;
|
|
3069
|
-
const coding = definition.coding;
|
|
3070
|
-
internal.siteCoding = (coding === null || coding === void 0 ? void 0 : coding.code)
|
|
3071
|
-
? {
|
|
3072
|
-
code: coding.code,
|
|
3073
|
-
display: coding.display,
|
|
3074
|
-
system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
|
|
3075
|
-
}
|
|
3076
|
-
: undefined;
|
|
3077
|
-
if (definition.text) {
|
|
3078
|
-
internal.siteText = definition.text;
|
|
3079
|
-
}
|
|
3080
|
-
else if (!internal.siteText && ((_b = internal.siteLookupRequest) === null || _b === void 0 ? void 0 : _b.text)) {
|
|
3081
|
-
internal.siteText = internal.siteLookupRequest.text;
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
/**
|
|
3085
|
-
* Converts a body-site definition into a suggestion payload so all suggestion
|
|
3086
|
-
* sources share consistent structure.
|
|
3087
|
-
*/
|
|
3088
|
-
function definitionToSuggestion(definition) {
|
|
3089
|
-
var _a;
|
|
3090
|
-
const coding = definition.coding;
|
|
3091
|
-
if (!(coding === null || coding === void 0 ? void 0 : coding.code)) {
|
|
3092
|
-
return undefined;
|
|
3093
|
-
}
|
|
3094
|
-
return {
|
|
3095
|
-
coding: {
|
|
3096
|
-
code: coding.code,
|
|
3097
|
-
display: coding.display,
|
|
3098
|
-
system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
|
|
3099
|
-
},
|
|
3100
|
-
text: definition.text
|
|
3101
|
-
};
|
|
3102
|
-
}
|
|
3103
|
-
/**
|
|
3104
|
-
* Inserts a suggestion into a deduplicated map keyed by system and code.
|
|
3105
|
-
*/
|
|
3106
|
-
function addSuggestionToMap(map, suggestion) {
|
|
3107
|
-
var _a, _b;
|
|
3108
|
-
if (!suggestion) {
|
|
3109
|
-
return;
|
|
3110
|
-
}
|
|
3111
|
-
const coding = suggestion.coding;
|
|
3112
|
-
if (!(coding === null || coding === void 0 ? void 0 : coding.code)) {
|
|
3113
|
-
return;
|
|
3114
|
-
}
|
|
3115
|
-
const key = `${(_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM}|${coding.code}`;
|
|
3116
|
-
if (!map.has(key)) {
|
|
3117
|
-
map.set(key, {
|
|
3118
|
-
coding: {
|
|
3119
|
-
code: coding.code,
|
|
3120
|
-
display: coding.display,
|
|
3121
|
-
system: (_b = coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM
|
|
3122
|
-
},
|
|
3123
|
-
text: suggestion.text
|
|
3124
|
-
});
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
/**
|
|
3128
|
-
* Normalizes resolver outputs into a consistent array before merging them into
|
|
3129
|
-
* the suggestion map.
|
|
3130
|
-
*/
|
|
3131
|
-
function collectSuggestionResult(map, result) {
|
|
3132
|
-
if (!result) {
|
|
3133
|
-
return;
|
|
3134
|
-
}
|
|
3135
|
-
const suggestions = Array.isArray(result)
|
|
3136
|
-
? result
|
|
3137
|
-
: typeof result === "object" && "suggestions" in result
|
|
3138
|
-
? result.suggestions
|
|
3139
|
-
: [result];
|
|
3140
|
-
for (const suggestion of suggestions) {
|
|
3141
|
-
addSuggestionToMap(map, suggestion);
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
function findAdditionalInstructionDefinition(text, canonical) {
|
|
3145
|
-
if (!canonical) {
|
|
3146
|
-
return undefined;
|
|
3147
|
-
}
|
|
3148
|
-
for (const entry of maps_1.DEFAULT_ADDITIONAL_INSTRUCTION_ENTRIES) {
|
|
3149
|
-
if (!entry.canonical) {
|
|
3150
|
-
continue;
|
|
3151
|
-
}
|
|
3152
|
-
// Check for exact canonical match first
|
|
3153
|
-
if (entry.canonical === canonical) {
|
|
3154
|
-
return entry.definition;
|
|
3155
|
-
}
|
|
3156
|
-
// Avoid broad includes checks (like "with" matching "with meal")
|
|
3157
|
-
// to prevent leakage of common connectors into additional instructions.
|
|
3158
|
-
for (const term of entry.terms) {
|
|
3159
|
-
const normalizedTerm = (0, maps_1.normalizeAdditionalInstructionKey)(term);
|
|
3160
|
-
if (!normalizedTerm) {
|
|
3161
|
-
continue;
|
|
3162
|
-
}
|
|
3163
|
-
if (canonical.includes(normalizedTerm) || normalizedTerm.includes(canonical)) {
|
|
3164
|
-
return entry.definition;
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
}
|
|
3168
|
-
return undefined;
|
|
3169
|
-
}
|
|
3170
|
-
const BODY_SITE_ADJECTIVE_SUFFIXES = [
|
|
3171
|
-
"al",
|
|
3172
|
-
"ial",
|
|
3173
|
-
"ual",
|
|
3174
|
-
"ic",
|
|
3175
|
-
"ous",
|
|
3176
|
-
"ive",
|
|
3177
|
-
"ary",
|
|
3178
|
-
"ory",
|
|
3179
|
-
"atic",
|
|
3180
|
-
"etic",
|
|
3181
|
-
"ular",
|
|
3182
|
-
"otic",
|
|
3183
|
-
"ile",
|
|
3184
|
-
"eal",
|
|
3185
|
-
"inal",
|
|
3186
|
-
"aneal",
|
|
3187
|
-
"enal"
|
|
3188
|
-
];
|
|
3189
|
-
const DEFAULT_SITE_SYNONYM_KEYS = (() => {
|
|
3190
|
-
const map = new Map();
|
|
3191
|
-
for (const [key, definition] of (0, object_1.objectEntries)(maps_1.DEFAULT_BODY_SITE_SNOMED)) {
|
|
3192
|
-
if (!definition) {
|
|
3193
|
-
continue;
|
|
3194
|
-
}
|
|
3195
|
-
const normalized = key.trim();
|
|
3196
|
-
if (!normalized) {
|
|
3197
|
-
continue;
|
|
3198
|
-
}
|
|
3199
|
-
const existing = map.get(definition);
|
|
3200
|
-
if (existing) {
|
|
3201
|
-
if (existing.indexOf(normalized) === -1) {
|
|
3202
|
-
existing.push(normalized);
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
else {
|
|
3206
|
-
map.set(definition, [normalized]);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
return map;
|
|
3210
|
-
})();
|
|
3211
|
-
function normalizeSiteDisplayText(text, customSiteMap) {
|
|
3212
|
-
var _a;
|
|
3213
|
-
const trimmed = text.trim();
|
|
3214
|
-
if (!trimmed) {
|
|
3215
|
-
return trimmed;
|
|
3216
|
-
}
|
|
3217
|
-
const canonicalInput = (0, maps_1.normalizeBodySiteKey)(trimmed);
|
|
3218
|
-
if (!canonicalInput) {
|
|
3219
|
-
return trimmed;
|
|
3220
|
-
}
|
|
3221
|
-
const resolvePreferred = (canonical) => {
|
|
3222
|
-
var _a;
|
|
3223
|
-
const definition = (_a = lookupBodySiteDefinition(customSiteMap, canonical)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonical];
|
|
3224
|
-
if (!definition) {
|
|
3225
|
-
return undefined;
|
|
3226
|
-
}
|
|
3227
|
-
const preferred = pickPreferredBodySitePhrase(canonical, definition, customSiteMap);
|
|
3228
|
-
const textValue = preferred !== null && preferred !== void 0 ? preferred : canonical;
|
|
3229
|
-
const normalized = (0, maps_1.normalizeBodySiteKey)(textValue);
|
|
3230
|
-
if (!normalized) {
|
|
3231
|
-
return undefined;
|
|
3232
|
-
}
|
|
3233
|
-
return { text: textValue, canonical: normalized };
|
|
3234
|
-
};
|
|
3235
|
-
if (isAdjectivalSitePhrase(canonicalInput)) {
|
|
3236
|
-
const direct = resolvePreferred(canonicalInput);
|
|
3237
|
-
return (_a = direct === null || direct === void 0 ? void 0 : direct.text) !== null && _a !== void 0 ? _a : trimmed;
|
|
3238
|
-
}
|
|
3239
|
-
const words = canonicalInput.split(/\s+/).filter((word) => word.length > 0);
|
|
3240
|
-
for (let i = 1; i < words.length; i++) {
|
|
3241
|
-
const prefix = words.slice(0, i);
|
|
3242
|
-
if (!prefix.every((word) => isAdjectivalSitePhrase(word))) {
|
|
3243
|
-
continue;
|
|
3244
|
-
}
|
|
3245
|
-
const candidateCanonical = words.slice(i).join(" ");
|
|
3246
|
-
if (!candidateCanonical) {
|
|
3247
|
-
continue;
|
|
3248
|
-
}
|
|
3249
|
-
const candidatePreferred = resolvePreferred(candidateCanonical);
|
|
3250
|
-
if (!candidatePreferred) {
|
|
3251
|
-
continue;
|
|
3252
|
-
}
|
|
3253
|
-
const prefixMatches = prefix.every((word) => {
|
|
3254
|
-
const normalizedPrefix = resolvePreferred(word);
|
|
3255
|
-
return (normalizedPrefix !== undefined &&
|
|
3256
|
-
normalizedPrefix.canonical === candidatePreferred.canonical);
|
|
3257
|
-
});
|
|
3258
|
-
if (!prefixMatches) {
|
|
3259
|
-
continue;
|
|
3260
|
-
}
|
|
3261
|
-
return candidatePreferred.text;
|
|
3262
|
-
}
|
|
3263
|
-
return trimmed;
|
|
3264
|
-
}
|
|
3265
|
-
function pickPreferredBodySitePhrase(canonical, definition, customSiteMap) {
|
|
3266
|
-
const synonyms = new Set();
|
|
3267
|
-
synonyms.add(canonical);
|
|
3268
|
-
if (definition.aliases) {
|
|
3269
|
-
for (const alias of definition.aliases) {
|
|
3270
|
-
const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
|
|
3271
|
-
if (normalizedAlias) {
|
|
3272
|
-
synonyms.add(normalizedAlias);
|
|
3273
|
-
}
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
const defaultSynonyms = DEFAULT_SITE_SYNONYM_KEYS.get(definition);
|
|
3277
|
-
if (defaultSynonyms) {
|
|
3278
|
-
for (const synonym of defaultSynonyms) {
|
|
3279
|
-
synonyms.add(synonym);
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
if (customSiteMap) {
|
|
3283
|
-
for (const [key, candidate] of (0, object_1.objectEntries)(customSiteMap)) {
|
|
3284
|
-
if (!candidate) {
|
|
3285
|
-
continue;
|
|
3286
|
-
}
|
|
3287
|
-
if (candidate === definition) {
|
|
3288
|
-
const normalizedKey = (0, maps_1.normalizeBodySiteKey)(key);
|
|
3289
|
-
if (normalizedKey) {
|
|
3290
|
-
synonyms.add(normalizedKey);
|
|
3291
|
-
}
|
|
3292
|
-
if (candidate.aliases) {
|
|
3293
|
-
for (const alias of candidate.aliases) {
|
|
3294
|
-
const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
|
|
3295
|
-
if (normalizedAlias) {
|
|
3296
|
-
synonyms.add(normalizedAlias);
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
const candidates = Array.from(synonyms).filter((phrase) => phrase && !isAdjectivalSitePhrase(phrase));
|
|
3304
|
-
if (!candidates.length) {
|
|
3305
|
-
return undefined;
|
|
3306
|
-
}
|
|
3307
|
-
candidates.sort((a, b) => scoreBodySitePhrase(b) - scoreBodySitePhrase(a));
|
|
3308
|
-
const best = candidates[0];
|
|
3309
|
-
if (!best) {
|
|
3310
|
-
return undefined;
|
|
3311
|
-
}
|
|
3312
|
-
if ((0, maps_1.normalizeBodySiteKey)(best) === canonical) {
|
|
3313
|
-
return undefined;
|
|
3314
|
-
}
|
|
3315
|
-
return best;
|
|
3316
|
-
}
|
|
3317
|
-
function scoreBodySitePhrase(phrase) {
|
|
3318
|
-
const lower = phrase.toLowerCase();
|
|
3319
|
-
const words = lower.split(/\s+/).filter((part) => part.length > 0);
|
|
3320
|
-
let score = 0;
|
|
3321
|
-
if (!/(structure|region|entire|proper|body)/.test(lower)) {
|
|
3322
|
-
score += 3;
|
|
3323
|
-
}
|
|
3324
|
-
if (!lower.includes(" of ")) {
|
|
3325
|
-
score += 1;
|
|
3326
|
-
}
|
|
3327
|
-
if (words.length <= 2) {
|
|
3328
|
-
score += 1;
|
|
3329
|
-
}
|
|
3330
|
-
if (words.length === 1) {
|
|
3331
|
-
score += 0.5;
|
|
3332
|
-
}
|
|
3333
|
-
score -= words.length * 0.2;
|
|
3334
|
-
score -= lower.length * 0.01;
|
|
3335
|
-
return score;
|
|
3336
|
-
}
|
|
3337
|
-
function isAdjectivalSitePhrase(phrase) {
|
|
3338
|
-
const normalized = phrase.trim().toLowerCase();
|
|
3339
|
-
if (!normalized) {
|
|
3340
|
-
return false;
|
|
3341
|
-
}
|
|
3342
|
-
const words = normalized.split(/\s+/).filter((word) => word.length > 0);
|
|
3343
|
-
if (words.length !== 1) {
|
|
3344
|
-
return false;
|
|
3345
|
-
}
|
|
3346
|
-
const last = words[words.length - 1];
|
|
3347
|
-
if (last.length <= 3) {
|
|
3348
|
-
return false;
|
|
3349
|
-
}
|
|
3350
|
-
return BODY_SITE_ADJECTIVE_SUFFIXES.some((suffix) => last.endsWith(suffix));
|
|
3351
|
-
}
|
|
3352
|
-
function collectAdditionalInstructions(internal, tokens) {
|
|
3353
|
-
var _a, _b, _c, _d, _e, _f;
|
|
3354
|
-
if (internal.additionalInstructions.length) {
|
|
3355
|
-
return;
|
|
3356
|
-
}
|
|
3357
|
-
const punctuationOnly = /^[;:.,-]+$/;
|
|
3358
|
-
const trailing = [];
|
|
3359
|
-
let expectedIndex;
|
|
3360
|
-
for (let cursor = tokens.length - 1; cursor >= 0; cursor--) {
|
|
3361
|
-
const token = tokens[cursor];
|
|
3362
|
-
if (!token) {
|
|
3363
|
-
continue;
|
|
3364
|
-
}
|
|
3365
|
-
if (internal.consumed.has(token.index)) {
|
|
3366
|
-
if (trailing.length > 0) {
|
|
3367
|
-
break;
|
|
3368
|
-
}
|
|
3369
|
-
continue;
|
|
3370
|
-
}
|
|
3371
|
-
if (expectedIndex !== undefined && token.index !== expectedIndex - 1) {
|
|
3372
|
-
break;
|
|
3373
|
-
}
|
|
3374
|
-
trailing.unshift(token);
|
|
3375
|
-
expectedIndex = token.index;
|
|
3376
|
-
}
|
|
3377
|
-
if (!trailing.length) {
|
|
3378
|
-
return;
|
|
3379
|
-
}
|
|
3380
|
-
const contentTokens = trailing.filter((token) => !punctuationOnly.test(token.original));
|
|
3381
|
-
if (!contentTokens.length) {
|
|
3382
|
-
return;
|
|
3383
|
-
}
|
|
3384
|
-
const trailingIndices = trailing.map((token) => token.index).sort((a, b) => a - b);
|
|
3385
|
-
const lastIndex = trailingIndices[trailingIndices.length - 1];
|
|
3386
|
-
for (let i = lastIndex + 1; i < tokens.length; i++) {
|
|
3387
|
-
const nextToken = tokens[i];
|
|
3388
|
-
if (!nextToken) {
|
|
3389
|
-
continue;
|
|
3390
|
-
}
|
|
3391
|
-
if (!internal.consumed.has(nextToken.index)) {
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
const joined = contentTokens
|
|
3396
|
-
.map((token) => token.original)
|
|
3397
|
-
.join(" ")
|
|
3398
|
-
.replace(/\s+/g, " ")
|
|
3399
|
-
.trim();
|
|
3400
|
-
if (!joined) {
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const contentIndices = contentTokens.map((token) => token.index).sort((a, b) => a - b);
|
|
3404
|
-
const lowerInput = internal.input.toLowerCase();
|
|
3405
|
-
let trailingRange;
|
|
3406
|
-
let searchEnd = lowerInput.length;
|
|
3407
|
-
let rangeStart;
|
|
3408
|
-
let rangeEnd;
|
|
3409
|
-
for (let i = contentTokens.length - 1; i >= 0; i--) {
|
|
3410
|
-
const fragment = contentTokens[i].original.trim();
|
|
3411
|
-
if (!fragment) {
|
|
3412
|
-
continue;
|
|
3413
|
-
}
|
|
3414
|
-
const lowerFragment = fragment.toLowerCase();
|
|
3415
|
-
const foundIndex = lowerInput.lastIndexOf(lowerFragment, searchEnd - 1);
|
|
3416
|
-
if (foundIndex === -1) {
|
|
3417
|
-
rangeStart = undefined;
|
|
3418
|
-
rangeEnd = undefined;
|
|
3419
|
-
break;
|
|
3420
|
-
}
|
|
3421
|
-
rangeStart = foundIndex;
|
|
3422
|
-
if (rangeEnd === undefined) {
|
|
3423
|
-
rangeEnd = foundIndex + lowerFragment.length;
|
|
3424
|
-
}
|
|
3425
|
-
searchEnd = foundIndex;
|
|
3426
|
-
}
|
|
3427
|
-
if (rangeStart !== undefined && rangeEnd !== undefined) {
|
|
3428
|
-
trailingRange = { start: rangeStart, end: rangeEnd };
|
|
3429
|
-
}
|
|
3430
|
-
const range = trailingRange !== null && trailingRange !== void 0 ? trailingRange : computeTokenRange(internal.input, tokens, contentIndices);
|
|
3431
|
-
let separatorDetected = false;
|
|
3432
|
-
if (range) {
|
|
3433
|
-
for (let cursor = range.start - 1; cursor >= 0; cursor--) {
|
|
3434
|
-
const ch = internal.input[cursor];
|
|
3435
|
-
if (ch === "\n" || ch === "\r") {
|
|
3436
|
-
separatorDetected = true;
|
|
3437
|
-
break;
|
|
3438
|
-
}
|
|
3439
|
-
if (/\s/.test(ch)) {
|
|
3440
|
-
continue;
|
|
3441
|
-
}
|
|
3442
|
-
if (/-|;|:|\.|\,/.test(ch)) {
|
|
3443
|
-
separatorDetected = true;
|
|
3444
|
-
}
|
|
3445
|
-
break;
|
|
3446
|
-
}
|
|
3447
|
-
}
|
|
3448
|
-
const sourceText = range
|
|
3449
|
-
? internal.input.slice(range.start, range.end)
|
|
3450
|
-
: joined;
|
|
3451
|
-
const normalized = sourceText
|
|
3452
|
-
.replace(/\s*[-:]+\s*/g, "; ")
|
|
3453
|
-
.replace(/\s*(?:\r?\n)+\s*/g, "; ")
|
|
3454
|
-
.replace(/\s+/g, " ");
|
|
3455
|
-
const segments = normalized
|
|
3456
|
-
.split(/(?:;|\.)/)
|
|
3457
|
-
.map((segment) => segment.trim())
|
|
3458
|
-
.filter((segment) => segment.length > 0);
|
|
3459
|
-
// If no punctuation was detected, we only collect if at least one segment matches a known definition.
|
|
3460
|
-
// This avoids capturing random trailing text as instructions unless it's codified.
|
|
3461
|
-
if (!separatorDetected && !/[-;:.]/.test(sourceText)) {
|
|
3462
|
-
const hasKnownDefinition = segments.some((phrase) => {
|
|
3463
|
-
const canonical = (0, maps_1.normalizeAdditionalInstructionKey)(phrase);
|
|
3464
|
-
return (maps_1.DEFAULT_ADDITIONAL_INSTRUCTION_DEFINITIONS[canonical] ||
|
|
3465
|
-
findAdditionalInstructionDefinition(phrase, canonical));
|
|
3466
|
-
});
|
|
3467
|
-
if (!hasKnownDefinition) {
|
|
3468
|
-
return;
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3471
|
-
const phrases = segments.length ? segments : [joined];
|
|
3472
|
-
const seen = new Set();
|
|
3473
|
-
const instructions = [];
|
|
3474
|
-
for (const phrase of phrases) {
|
|
3475
|
-
const canonical = (0, maps_1.normalizeAdditionalInstructionKey)(phrase);
|
|
3476
|
-
const definition = (_a = maps_1.DEFAULT_ADDITIONAL_INSTRUCTION_DEFINITIONS[canonical]) !== null && _a !== void 0 ? _a : findAdditionalInstructionDefinition(phrase, canonical);
|
|
3477
|
-
const key = ((_b = definition === null || definition === void 0 ? void 0 : definition.coding) === null || _b === void 0 ? void 0 : _b.code)
|
|
3478
|
-
? `code:${(_c = definition.coding.system) !== null && _c !== void 0 ? _c : SNOMED_SYSTEM}|${definition.coding.code}`
|
|
3479
|
-
: canonical
|
|
3480
|
-
? `text:${canonical}`
|
|
3481
|
-
: phrase.toLowerCase();
|
|
3482
|
-
if (key && seen.has(key)) {
|
|
3483
|
-
continue;
|
|
3484
|
-
}
|
|
3485
|
-
seen.add(key);
|
|
3486
|
-
if (definition) {
|
|
3487
|
-
instructions.push({
|
|
3488
|
-
text: (_d = definition.text) !== null && _d !== void 0 ? _d : phrase,
|
|
3489
|
-
coding: ((_e = definition.coding) === null || _e === void 0 ? void 0 : _e.code)
|
|
3490
|
-
? {
|
|
3491
|
-
code: definition.coding.code,
|
|
3492
|
-
display: definition.coding.display,
|
|
3493
|
-
system: (_f = definition.coding.system) !== null && _f !== void 0 ? _f : SNOMED_SYSTEM,
|
|
3494
|
-
i18n: definition.i18n
|
|
3495
|
-
}
|
|
3496
|
-
: undefined
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
else if (!MEAL_CONTEXT_CONNECTORS.has(phrase.toLowerCase())) {
|
|
3500
|
-
instructions.push({ text: phrase });
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
if (instructions.length) {
|
|
3504
|
-
internal.additionalInstructions = instructions;
|
|
3505
|
-
for (const token of trailing) {
|
|
3506
|
-
mark(internal.consumed, token);
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
function determinePrnReasonCutoff(tokens, sourceText) {
|
|
3511
|
-
const separatorIndex = findPrnReasonSeparator(sourceText);
|
|
3512
|
-
if (separatorIndex === undefined) {
|
|
3513
|
-
return undefined;
|
|
3514
|
-
}
|
|
3515
|
-
const lowerSource = sourceText.toLowerCase();
|
|
3516
|
-
let searchOffset = 0;
|
|
3517
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
3518
|
-
const token = tokens[i];
|
|
3519
|
-
const fragment = token.original.trim();
|
|
3520
|
-
if (!fragment) {
|
|
3521
|
-
continue;
|
|
3522
|
-
}
|
|
3523
|
-
const lowerFragment = fragment.toLowerCase();
|
|
3524
|
-
const position = lowerSource.indexOf(lowerFragment, searchOffset);
|
|
3525
|
-
if (position === -1) {
|
|
3526
|
-
continue;
|
|
3527
|
-
}
|
|
3528
|
-
const end = position + lowerFragment.length;
|
|
3529
|
-
searchOffset = end;
|
|
3530
|
-
if (position >= separatorIndex) {
|
|
3531
|
-
return i;
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
return undefined;
|
|
3535
|
-
}
|
|
3536
|
-
function findPrnReasonSeparator(sourceText) {
|
|
3537
|
-
var _a;
|
|
3538
|
-
for (let i = 0; i < sourceText.length; i++) {
|
|
3539
|
-
const ch = sourceText[i];
|
|
3540
|
-
if (ch === "\n" || ch === "\r") {
|
|
3541
|
-
if (sourceText.slice(i + 1).trim().length > 0) {
|
|
3542
|
-
return i;
|
|
3543
|
-
}
|
|
3544
|
-
continue;
|
|
3545
|
-
}
|
|
3546
|
-
if (ch === ";") {
|
|
3547
|
-
if (sourceText.slice(i + 1).trim().length > 0) {
|
|
3548
|
-
return i;
|
|
3549
|
-
}
|
|
3550
|
-
continue;
|
|
3551
|
-
}
|
|
3552
|
-
if (ch === "-") {
|
|
3553
|
-
const prev = sourceText[i - 1];
|
|
3554
|
-
const next = sourceText[i + 1];
|
|
3555
|
-
const hasWhitespaceAround = (!prev || /\s/.test(prev)) && (!next || /\s/.test(next));
|
|
3556
|
-
if (hasWhitespaceAround && sourceText.slice(i + 1).trim().length > 0) {
|
|
3557
|
-
return i;
|
|
3558
|
-
}
|
|
3559
|
-
continue;
|
|
3560
|
-
}
|
|
3561
|
-
if (ch === ":" || ch === ".") {
|
|
3562
|
-
const rest = sourceText.slice(i + 1);
|
|
3563
|
-
if (!rest.trim().length) {
|
|
3564
|
-
continue;
|
|
3565
|
-
}
|
|
3566
|
-
const nextChar = rest.replace(/^\s+/, "")[0];
|
|
3567
|
-
if (!nextChar) {
|
|
3568
|
-
continue;
|
|
3569
|
-
}
|
|
3570
|
-
if (ch === "." &&
|
|
3571
|
-
/[0-9]/.test((_a = sourceText[i - 1]) !== null && _a !== void 0 ? _a : "") &&
|
|
3572
|
-
/[0-9]/.test(nextChar)) {
|
|
3573
|
-
continue;
|
|
3574
|
-
}
|
|
3575
|
-
return i;
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
return undefined;
|
|
3579
|
-
}
|
|
3580
|
-
function findTrailingPrnSiteSuffix(tokens, internal, options) {
|
|
3581
|
-
var _a;
|
|
3582
|
-
let suffixStart;
|
|
3583
|
-
let hasSiteHint = false;
|
|
3584
|
-
let hasConnector = false;
|
|
3585
|
-
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
3586
|
-
const token = tokens[i];
|
|
3587
|
-
const lower = normalizeTokenLower(token);
|
|
3588
|
-
if (!lower) {
|
|
3589
|
-
if (suffixStart !== undefined && token.original.trim()) {
|
|
3590
|
-
break;
|
|
3591
|
-
}
|
|
3592
|
-
continue;
|
|
3593
|
-
}
|
|
3594
|
-
if (isBodySiteHint(lower, internal.customSiteHints)) {
|
|
3595
|
-
hasSiteHint = true;
|
|
3596
|
-
suffixStart = i;
|
|
3597
|
-
continue;
|
|
3598
|
-
}
|
|
3599
|
-
if (suffixStart !== undefined) {
|
|
3600
|
-
if (SITE_CONNECTORS.has(lower)) {
|
|
3601
|
-
hasConnector = true;
|
|
3602
|
-
suffixStart = i;
|
|
3603
|
-
continue;
|
|
3604
|
-
}
|
|
3605
|
-
if (SITE_FILLER_WORDS.has(lower) || ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
3606
|
-
suffixStart = i;
|
|
3607
|
-
continue;
|
|
3608
|
-
}
|
|
3609
|
-
}
|
|
3610
|
-
if (suffixStart !== undefined) {
|
|
3611
|
-
break;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
if (!hasSiteHint || !hasConnector || suffixStart === undefined || suffixStart === 0) {
|
|
3615
|
-
return undefined;
|
|
3616
|
-
}
|
|
3617
|
-
const suffixTokens = tokens.slice(suffixStart);
|
|
3618
|
-
const siteWords = [];
|
|
3619
|
-
const siteHintTokens = [];
|
|
3620
|
-
for (const token of suffixTokens) {
|
|
3621
|
-
const trimmed = token.original.trim();
|
|
3622
|
-
if (!trimmed) {
|
|
3623
|
-
continue;
|
|
3624
|
-
}
|
|
3625
|
-
const lower = normalizeTokenLower(token);
|
|
3626
|
-
if (SITE_CONNECTORS.has(lower) ||
|
|
3627
|
-
SITE_FILLER_WORDS.has(lower) ||
|
|
3628
|
-
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
3629
|
-
continue;
|
|
3630
|
-
}
|
|
3631
|
-
siteHintTokens.push(token);
|
|
3632
|
-
siteWords.push(trimmed);
|
|
3633
|
-
}
|
|
3634
|
-
if (!siteWords.length) {
|
|
3635
|
-
return undefined;
|
|
3636
|
-
}
|
|
3637
|
-
const sitePhrase = siteWords.join(" ");
|
|
3638
|
-
const canonical = (0, maps_1.normalizeBodySiteKey)(sitePhrase);
|
|
3639
|
-
if (!canonical) {
|
|
3640
|
-
return undefined;
|
|
3641
|
-
}
|
|
3642
|
-
const definition = (_a = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonical];
|
|
3643
|
-
if (!definition) {
|
|
3644
|
-
return undefined;
|
|
3645
|
-
}
|
|
3646
|
-
return {
|
|
3647
|
-
tokens: siteHintTokens,
|
|
3648
|
-
startIndex: suffixStart
|
|
3649
|
-
};
|
|
3650
|
-
}
|
|
3651
|
-
function lookupPrnReasonDefinition(map, canonical) {
|
|
3652
|
-
if (!map) {
|
|
3653
|
-
return undefined;
|
|
3654
|
-
}
|
|
3655
|
-
const direct = map[canonical];
|
|
3656
|
-
if (direct) {
|
|
3657
|
-
return direct;
|
|
3658
|
-
}
|
|
3659
|
-
for (const [key, definition] of (0, object_1.objectEntries)(map)) {
|
|
3660
|
-
if ((0, maps_1.normalizePrnReasonKey)(key) === canonical) {
|
|
3661
|
-
return definition;
|
|
3662
|
-
}
|
|
3663
|
-
if (definition.aliases) {
|
|
3664
|
-
for (const alias of definition.aliases) {
|
|
3665
|
-
if ((0, maps_1.normalizePrnReasonKey)(alias) === canonical) {
|
|
3666
|
-
return definition;
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
}
|
|
3671
|
-
return undefined;
|
|
3672
|
-
}
|
|
3673
|
-
function pickPrnReasonSelection(selections, request) {
|
|
3674
|
-
if (!selections) {
|
|
3675
|
-
return undefined;
|
|
3676
|
-
}
|
|
3677
|
-
const canonical = request.canonical;
|
|
3678
|
-
const normalizedText = (0, maps_1.normalizePrnReasonKey)(request.text);
|
|
3679
|
-
const requestRange = request.range;
|
|
3680
|
-
for (const selection of toArray(selections)) {
|
|
3681
|
-
if (!selection) {
|
|
3682
|
-
continue;
|
|
3683
|
-
}
|
|
3684
|
-
let matched = false;
|
|
3685
|
-
if (selection.range) {
|
|
3686
|
-
if (!requestRange) {
|
|
3687
|
-
continue;
|
|
3688
|
-
}
|
|
3689
|
-
if (selection.range.start !== requestRange.start ||
|
|
3690
|
-
selection.range.end !== requestRange.end) {
|
|
3691
|
-
continue;
|
|
3692
|
-
}
|
|
3693
|
-
matched = true;
|
|
3694
|
-
}
|
|
3695
|
-
if (selection.canonical) {
|
|
3696
|
-
if ((0, maps_1.normalizePrnReasonKey)(selection.canonical) !== canonical) {
|
|
3697
|
-
continue;
|
|
3698
|
-
}
|
|
3699
|
-
matched = true;
|
|
3700
|
-
}
|
|
3701
|
-
else if (selection.text) {
|
|
3702
|
-
const normalizedSelection = (0, maps_1.normalizePrnReasonKey)(selection.text);
|
|
3703
|
-
if (normalizedSelection !== canonical && normalizedSelection !== normalizedText) {
|
|
3704
|
-
continue;
|
|
3705
|
-
}
|
|
3706
|
-
matched = true;
|
|
3707
|
-
}
|
|
3708
|
-
if (!selection.range && !selection.canonical && !selection.text) {
|
|
3709
|
-
continue;
|
|
3710
|
-
}
|
|
3711
|
-
if (matched) {
|
|
3712
|
-
return selection.resolution;
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3715
|
-
return undefined;
|
|
3716
|
-
}
|
|
3717
|
-
function applyPrnReasonDefinition(internal, definition) {
|
|
3718
|
-
var _a;
|
|
3719
|
-
const coding = definition.coding;
|
|
3720
|
-
internal.asNeededReasonCoding = (coding === null || coding === void 0 ? void 0 : coding.code)
|
|
3721
|
-
? {
|
|
3722
|
-
code: coding.code,
|
|
3723
|
-
display: coding.display,
|
|
3724
|
-
system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM,
|
|
3725
|
-
i18n: definition.i18n
|
|
3726
|
-
}
|
|
3727
|
-
: undefined;
|
|
3728
|
-
if (definition.text && !internal.asNeededReason) {
|
|
3729
|
-
internal.asNeededReason = definition.text;
|
|
3730
|
-
}
|
|
3731
|
-
}
|
|
3732
|
-
function definitionToPrnSuggestion(definition) {
|
|
3733
|
-
var _a, _b, _c, _d;
|
|
3734
|
-
return {
|
|
3735
|
-
coding: ((_a = definition.coding) === null || _a === void 0 ? void 0 : _a.code)
|
|
3736
|
-
? {
|
|
3737
|
-
code: definition.coding.code,
|
|
3738
|
-
display: definition.coding.display,
|
|
3739
|
-
system: (_b = definition.coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM
|
|
3740
|
-
}
|
|
3741
|
-
: undefined,
|
|
3742
|
-
text: (_c = definition.text) !== null && _c !== void 0 ? _c : (_d = definition.coding) === null || _d === void 0 ? void 0 : _d.display
|
|
3743
|
-
};
|
|
3744
|
-
}
|
|
3745
|
-
function addReasonSuggestionToMap(map, suggestion) {
|
|
3746
|
-
var _a;
|
|
3747
|
-
if (!suggestion) {
|
|
3748
|
-
return;
|
|
3749
|
-
}
|
|
3750
|
-
const coding = suggestion.coding;
|
|
3751
|
-
const key = (coding === null || coding === void 0 ? void 0 : coding.code)
|
|
3752
|
-
? `${(_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM}|${coding.code}`
|
|
3753
|
-
: suggestion.text
|
|
3754
|
-
? `text:${suggestion.text.toLowerCase()}`
|
|
3755
|
-
: undefined;
|
|
3756
|
-
if (!key || map.has(key)) {
|
|
3757
|
-
return;
|
|
3758
|
-
}
|
|
3759
|
-
map.set(key, suggestion);
|
|
3760
|
-
}
|
|
3761
|
-
function collectReasonSuggestionResult(map, result) {
|
|
3762
|
-
if (!result) {
|
|
3763
|
-
return;
|
|
3764
|
-
}
|
|
3765
|
-
const suggestions = Array.isArray(result)
|
|
3766
|
-
? result
|
|
3767
|
-
: typeof result === "object" && "suggestions" in result
|
|
3768
|
-
? result.suggestions
|
|
3769
|
-
: [result];
|
|
3770
|
-
for (const suggestion of suggestions) {
|
|
3771
|
-
addReasonSuggestionToMap(map, suggestion);
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
function collectDefaultPrnReasonDefinitions(request) {
|
|
3775
|
-
const canonical = request.canonical;
|
|
3776
|
-
const normalized = request.normalized;
|
|
3777
|
-
const seen = new Set();
|
|
3778
|
-
for (const entry of maps_1.DEFAULT_PRN_REASON_ENTRIES) {
|
|
3779
|
-
if (!entry.canonical) {
|
|
3780
|
-
continue;
|
|
3781
|
-
}
|
|
3782
|
-
if (entry.canonical === canonical) {
|
|
3783
|
-
seen.add(entry.definition);
|
|
3784
|
-
continue;
|
|
3785
|
-
}
|
|
3786
|
-
if (canonical && (entry.canonical.includes(canonical) || canonical.includes(entry.canonical))) {
|
|
3787
|
-
seen.add(entry.definition);
|
|
3788
|
-
continue;
|
|
3789
|
-
}
|
|
3790
|
-
for (const term of entry.terms) {
|
|
3791
|
-
const normalizedTerm = (0, maps_1.normalizePrnReasonKey)(term);
|
|
3792
|
-
if (!normalizedTerm) {
|
|
3793
|
-
continue;
|
|
3794
|
-
}
|
|
3795
|
-
if (canonical && canonical.includes(normalizedTerm)) {
|
|
3796
|
-
seen.add(entry.definition);
|
|
3797
|
-
break;
|
|
3798
|
-
}
|
|
3799
|
-
if (normalized.includes(normalizedTerm)) {
|
|
3800
|
-
seen.add(entry.definition);
|
|
3801
|
-
break;
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
}
|
|
3805
|
-
if (!seen.size) {
|
|
3806
|
-
for (const entry of maps_1.DEFAULT_PRN_REASON_ENTRIES) {
|
|
3807
|
-
seen.add(entry.definition);
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
return Array.from(seen);
|
|
3811
|
-
}
|
|
3812
|
-
function runPrnReasonResolutionSync(internal, options) {
|
|
3813
|
-
internal.prnReasonLookups = [];
|
|
3814
|
-
const request = internal.prnReasonLookupRequest;
|
|
3815
|
-
if (!request) {
|
|
3816
|
-
return;
|
|
3817
|
-
}
|
|
3818
|
-
const canonical = request.canonical;
|
|
3819
|
-
const selection = pickPrnReasonSelection(options === null || options === void 0 ? void 0 : options.prnReasonSelections, request);
|
|
3820
|
-
const customDefinition = lookupPrnReasonDefinition(options === null || options === void 0 ? void 0 : options.prnReasonMap, canonical);
|
|
3821
|
-
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
3822
|
-
if (!resolution) {
|
|
3823
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.prnReasonResolvers)) {
|
|
3824
|
-
const result = resolver(request);
|
|
3825
|
-
if (isPromise(result)) {
|
|
3826
|
-
throw new Error("PRN reason resolver returned a Promise; use parseSigAsync for asynchronous PRN reason resolution.");
|
|
3827
|
-
}
|
|
3828
|
-
if (result) {
|
|
3829
|
-
resolution = result;
|
|
3830
|
-
break;
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
const defaultDefinition = canonical ? maps_1.DEFAULT_PRN_REASON_DEFINITIONS[canonical] : undefined;
|
|
3835
|
-
if (!resolution && defaultDefinition) {
|
|
3836
|
-
resolution = defaultDefinition;
|
|
3837
|
-
}
|
|
3838
|
-
if (resolution) {
|
|
3839
|
-
applyPrnReasonDefinition(internal, resolution);
|
|
3840
|
-
}
|
|
3841
|
-
else {
|
|
3842
|
-
internal.asNeededReasonCoding = undefined;
|
|
3843
|
-
}
|
|
3844
|
-
const needsSuggestions = request.isProbe || !resolution;
|
|
3845
|
-
if (!needsSuggestions) {
|
|
3846
|
-
return;
|
|
3847
|
-
}
|
|
3848
|
-
const suggestionMap = new Map();
|
|
3849
|
-
if (selection) {
|
|
3850
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(selection));
|
|
3851
|
-
}
|
|
3852
|
-
if (customDefinition) {
|
|
3853
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(customDefinition));
|
|
3854
|
-
}
|
|
3855
|
-
if (defaultDefinition) {
|
|
3856
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(defaultDefinition));
|
|
3857
|
-
}
|
|
3858
|
-
for (const definition of collectDefaultPrnReasonDefinitions(request)) {
|
|
3859
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(definition));
|
|
3860
|
-
}
|
|
3861
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.prnReasonSuggestionResolvers)) {
|
|
3862
|
-
const result = resolver(request);
|
|
3863
|
-
if (isPromise(result)) {
|
|
3864
|
-
throw new Error("PRN reason suggestion resolver returned a Promise; use parseSigAsync for asynchronous PRN reason suggestions.");
|
|
3865
|
-
}
|
|
3866
|
-
collectReasonSuggestionResult(suggestionMap, result);
|
|
3867
|
-
}
|
|
3868
|
-
const suggestions = Array.from(suggestionMap.values());
|
|
3869
|
-
if (suggestions.length || request.isProbe) {
|
|
3870
|
-
internal.prnReasonLookups.push({ request, suggestions });
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
function runPrnReasonResolutionAsync(internal, options) {
|
|
3874
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
3875
|
-
internal.prnReasonLookups = [];
|
|
3876
|
-
const request = internal.prnReasonLookupRequest;
|
|
3877
|
-
if (!request) {
|
|
3878
|
-
return;
|
|
3879
|
-
}
|
|
3880
|
-
const canonical = request.canonical;
|
|
3881
|
-
const selection = pickPrnReasonSelection(options === null || options === void 0 ? void 0 : options.prnReasonSelections, request);
|
|
3882
|
-
const customDefinition = lookupPrnReasonDefinition(options === null || options === void 0 ? void 0 : options.prnReasonMap, canonical);
|
|
3883
|
-
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
3884
|
-
if (!resolution) {
|
|
3885
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.prnReasonResolvers)) {
|
|
3886
|
-
const result = yield resolver(request);
|
|
3887
|
-
if (result) {
|
|
3888
|
-
resolution = result;
|
|
3889
|
-
break;
|
|
3890
|
-
}
|
|
3891
|
-
}
|
|
3892
|
-
}
|
|
3893
|
-
const defaultDefinition = canonical ? maps_1.DEFAULT_PRN_REASON_DEFINITIONS[canonical] : undefined;
|
|
3894
|
-
if (!resolution && defaultDefinition) {
|
|
3895
|
-
resolution = defaultDefinition;
|
|
3896
|
-
}
|
|
3897
|
-
if (resolution) {
|
|
3898
|
-
applyPrnReasonDefinition(internal, resolution);
|
|
3899
|
-
}
|
|
3900
|
-
else {
|
|
3901
|
-
internal.asNeededReasonCoding = undefined;
|
|
3902
|
-
}
|
|
3903
|
-
const needsSuggestions = request.isProbe || !resolution;
|
|
3904
|
-
if (!needsSuggestions) {
|
|
3905
|
-
return;
|
|
3906
|
-
}
|
|
3907
|
-
const suggestionMap = new Map();
|
|
3908
|
-
if (selection) {
|
|
3909
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(selection));
|
|
3910
|
-
}
|
|
3911
|
-
if (customDefinition) {
|
|
3912
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(customDefinition));
|
|
3913
|
-
}
|
|
3914
|
-
if (defaultDefinition) {
|
|
3915
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(defaultDefinition));
|
|
3916
|
-
}
|
|
3917
|
-
for (const definition of collectDefaultPrnReasonDefinitions(request)) {
|
|
3918
|
-
addReasonSuggestionToMap(suggestionMap, definitionToPrnSuggestion(definition));
|
|
3919
|
-
}
|
|
3920
|
-
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.prnReasonSuggestionResolvers)) {
|
|
3921
|
-
const result = yield resolver(request);
|
|
3922
|
-
collectReasonSuggestionResult(suggestionMap, result);
|
|
3923
|
-
}
|
|
3924
|
-
const suggestions = Array.from(suggestionMap.values());
|
|
3925
|
-
if (suggestions.length || request.isProbe) {
|
|
3926
|
-
internal.prnReasonLookups.push({ request, suggestions });
|
|
3927
|
-
}
|
|
3928
|
-
});
|
|
3929
|
-
}
|
|
3930
|
-
/**
|
|
3931
|
-
* Wraps scalar or array configuration into an array to simplify iteration.
|
|
3932
|
-
*/
|
|
3933
|
-
function toArray(value) {
|
|
3934
|
-
if (!value) {
|
|
3935
|
-
return [];
|
|
3936
|
-
}
|
|
3937
|
-
return Array.isArray(value) ? value : [value];
|
|
3938
|
-
}
|
|
3939
|
-
/**
|
|
3940
|
-
* Detects thenables without relying on `instanceof Promise`, which can break
|
|
3941
|
-
* across execution contexts.
|
|
3942
|
-
*/
|
|
3943
|
-
function isPromise(value) {
|
|
3944
|
-
return !!value && typeof value.then === "function";
|
|
3945
|
-
}
|
|
3946
|
-
function normalizeUnit(token, options) {
|
|
3947
|
-
var _a;
|
|
3948
|
-
const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
|
|
3949
|
-
if (override) {
|
|
3950
|
-
return override;
|
|
3951
|
-
}
|
|
3952
|
-
const defaultUnit = enforceHouseholdUnitPolicy(maps_1.DEFAULT_UNIT_SYNONYMS[token], options);
|
|
3953
|
-
if (defaultUnit) {
|
|
3954
|
-
return defaultUnit;
|
|
3955
|
-
}
|
|
3956
|
-
return undefined;
|
|
3957
|
-
}
|
|
3958
|
-
function enforceHouseholdUnitPolicy(unit, options) {
|
|
3959
|
-
if (unit &&
|
|
3960
|
-
(options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
|
|
3961
|
-
HOUSEHOLD_VOLUME_UNIT_SET.has(unit.toLowerCase())) {
|
|
3962
|
-
return undefined;
|
|
3963
|
-
}
|
|
3964
|
-
return unit;
|
|
3965
|
-
}
|
|
3966
|
-
function isDiscreteUnit(unit) {
|
|
3967
|
-
if (!unit) {
|
|
3968
|
-
return false;
|
|
3969
|
-
}
|
|
3970
|
-
return DISCRETE_UNIT_SET.has(unit.trim().toLowerCase());
|
|
3971
|
-
}
|
|
3972
|
-
function inferUnitFromRouteHints(internal) {
|
|
3973
|
-
if (internal.routeCode) {
|
|
3974
|
-
const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[internal.routeCode];
|
|
3975
|
-
if (unit) {
|
|
3976
|
-
return unit;
|
|
3977
|
-
}
|
|
3978
|
-
}
|
|
3979
|
-
if (internal.routeText) {
|
|
3980
|
-
const normalized = internal.routeText.trim().toLowerCase();
|
|
3981
|
-
const synonym = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized];
|
|
3982
|
-
if (synonym) {
|
|
3983
|
-
const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[synonym.code];
|
|
3984
|
-
if (unit) {
|
|
3985
|
-
return unit;
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
|
-
}
|
|
3989
|
-
if (internal.siteText) {
|
|
3990
|
-
const unit = inferUnitFromSiteText(internal.siteText);
|
|
3991
|
-
if (unit) {
|
|
3992
|
-
return unit;
|
|
3993
|
-
}
|
|
3994
|
-
}
|
|
3995
|
-
return undefined;
|
|
3996
|
-
}
|
|
3997
|
-
function inferUnitFromSiteText(siteText) {
|
|
3998
|
-
for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
|
|
3999
|
-
if (pattern.test(siteText)) {
|
|
4000
|
-
const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[route];
|
|
4001
|
-
if (unit) {
|
|
4002
|
-
return unit;
|
|
4003
|
-
}
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
return undefined;
|
|
4007
|
-
}
|