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/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
- }