ezmedicationinput 0.1.3 → 0.1.6

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 CHANGED
@@ -110,6 +110,7 @@ const COMBO_EVENT_TIMINGS = {
110
110
  "after sleep": types_1.EventTiming["After Sleep"],
111
111
  "upon waking": types_1.EventTiming.Wake
112
112
  };
113
+ const MEAL_CONTEXT_CONNECTORS = new Set(["and", "or", "&", "+", "plus"]);
113
114
  // Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
114
115
  // logic bail early when the clinician already specified precise events.
115
116
  const SPECIFIC_MEAL_TIMINGS = new Set([
@@ -976,20 +977,31 @@ function applyWhenToken(internal, token, code) {
976
977
  }
977
978
  function parseMealContext(internal, tokens, index, code) {
978
979
  const token = tokens[index];
979
- const next = tokens[index + 1];
980
- if (!next || internal.consumed.has(next.index)) {
981
- applyWhenToken(internal, token, code);
982
- return;
983
- }
984
- const meal = maps_1.MEAL_KEYWORDS[next.lower];
985
- if (meal) {
980
+ let converted = 0;
981
+ for (let lookahead = index + 1; lookahead < tokens.length; lookahead++) {
982
+ const nextToken = tokens[lookahead];
983
+ if (internal.consumed.has(nextToken.index)) {
984
+ continue;
985
+ }
986
+ if (MEAL_CONTEXT_CONNECTORS.has(nextToken.lower)) {
987
+ mark(internal.consumed, nextToken);
988
+ continue;
989
+ }
990
+ const meal = maps_1.MEAL_KEYWORDS[nextToken.lower];
991
+ if (!meal) {
992
+ break;
993
+ }
986
994
  const whenCode = code === types_1.EventTiming["After Meal"]
987
995
  ? meal.pc
988
996
  : code === types_1.EventTiming["Before Meal"]
989
997
  ? meal.ac
990
998
  : code;
991
- applyWhenToken(internal, token, whenCode);
992
- mark(internal.consumed, next);
999
+ addWhen(internal.when, whenCode);
1000
+ mark(internal.consumed, nextToken);
1001
+ converted++;
1002
+ }
1003
+ if (converted > 0) {
1004
+ mark(internal.consumed, token);
993
1005
  return;
994
1006
  }
995
1007
  applyWhenToken(internal, token, code);
package/dist/suggest.js CHANGED
@@ -206,11 +206,40 @@ function normalizeSpacing(value) {
206
206
  .trim()
207
207
  .replace(/\s+/g, " ");
208
208
  }
209
+ function removeWhitespaceCharacters(value) {
210
+ for (let index = 0; index < value.length; index += 1) {
211
+ const code = value.charCodeAt(index);
212
+ if (code <= 32) {
213
+ const result = [];
214
+ for (let inner = 0; inner < value.length; inner += 1) {
215
+ const currentCode = value.charCodeAt(inner);
216
+ if (currentCode > 32) {
217
+ result.push(value.charAt(inner));
218
+ }
219
+ }
220
+ return result.join("");
221
+ }
222
+ }
223
+ return value;
224
+ }
225
+ function removeDashes(value) {
226
+ if (value.indexOf("-") === -1) {
227
+ return value;
228
+ }
229
+ const result = [];
230
+ for (let index = 0; index < value.length; index += 1) {
231
+ const char = value.charAt(index);
232
+ if (char !== "-") {
233
+ result.push(char);
234
+ }
235
+ }
236
+ return result.join("");
237
+ }
209
238
  function getUnitVariants(unit) {
210
239
  var _a;
211
240
  const canonical = (_a = resolveCanonicalUnit(unit)) !== null && _a !== void 0 ? _a : normalizeSpacing(unit);
212
241
  const normalizedCanonical = normalizeKey(canonical);
213
- const variants = new Set();
242
+ const variants = new Map();
214
243
  const push = (candidate) => {
215
244
  if (!candidate) {
216
245
  return;
@@ -219,7 +248,11 @@ function getUnitVariants(unit) {
219
248
  if (!normalizedCandidate) {
220
249
  return;
221
250
  }
222
- variants.add(normalizedCandidate);
251
+ const lower = normalizedCandidate.toLowerCase();
252
+ if (variants.has(lower)) {
253
+ return;
254
+ }
255
+ variants.set(lower, { value: normalizedCandidate, lower });
223
256
  };
224
257
  push(canonical);
225
258
  push(unit);
@@ -229,7 +262,7 @@ function getUnitVariants(unit) {
229
262
  push(candidate);
230
263
  }
231
264
  }
232
- return [...variants];
265
+ return [...variants.values()];
233
266
  }
234
267
  function buildIntervalTokens(input) {
235
268
  const intervals = new Set();
@@ -288,16 +321,22 @@ function buildWhenSequences() {
288
321
  }
289
322
  return sequences;
290
323
  }
291
- function tokenizeForMatching(value) {
324
+ const PRECOMPUTED_WHEN_SEQUENCES = buildWhenSequences();
325
+ function tokenizeLowercaseForMatching(value) {
292
326
  return value
293
- .toLowerCase()
294
327
  .split(/\s+/)
295
328
  .map((token) => token.replace(/^[^a-z0-9-]+|[^a-z0-9-]+$/g, ""))
296
329
  .filter((token) => token.length > 0)
297
330
  .filter((token) => !OPTIONAL_MATCH_TOKENS.has(token));
298
331
  }
332
+ function tokenizeForMatching(value) {
333
+ return tokenizeLowercaseForMatching(value.toLowerCase());
334
+ }
335
+ function canonicalizeLowercaseForMatching(value) {
336
+ return tokenizeLowercaseForMatching(value).join(" ");
337
+ }
299
338
  function canonicalizeForMatching(value) {
300
- return tokenizeForMatching(value).join(" ");
339
+ return canonicalizeLowercaseForMatching(value.toLowerCase());
301
340
  }
302
341
  function tokensMatch(prefixTokens, candidateTokens) {
303
342
  if (prefixTokens.length === 0) {
@@ -342,12 +381,13 @@ function buildUnitRoutePairs(contextUnit, options) {
342
381
  if (!cleanRoute) {
343
382
  return;
344
383
  }
345
- const key = `${normalizedUnit}::${normalizeKey(cleanRoute)}`;
384
+ const routeLower = cleanRoute.toLowerCase();
385
+ const key = `${normalizedUnit}::${routeLower}`;
346
386
  if (seen.has(key)) {
347
387
  return;
348
388
  }
349
389
  seen.add(key);
350
- pairs.push({ unit: canonicalUnit, route: cleanRoute });
390
+ pairs.push({ unit: canonicalUnit, route: cleanRoute, routeLower });
351
391
  };
352
392
  addPair(contextUnit);
353
393
  for (const preference of DEFAULT_UNIT_ROUTE_ORDER) {
@@ -403,133 +443,263 @@ function buildDoseValues(input) {
403
443
  }
404
444
  return [...values];
405
445
  }
406
- function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences) {
446
+ const CANDIDATE_FINGERPRINT_CACHE = new Map();
447
+ function getCandidateFingerprint(candidateLower) {
448
+ let fingerprint = CANDIDATE_FINGERPRINT_CACHE.get(candidateLower);
449
+ if (!fingerprint) {
450
+ fingerprint = {};
451
+ CANDIDATE_FINGERPRINT_CACHE.set(candidateLower, fingerprint);
452
+ }
453
+ return fingerprint;
454
+ }
455
+ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher) {
407
456
  const suggestions = [];
408
457
  const seen = new Set();
409
- const push = (value) => {
410
- const normalized = normalizeSpacing(value);
458
+ const doseVariantMap = new Map();
459
+ for (const dose of doseValues) {
460
+ const normalized = normalizeSpacing(dose);
411
461
  if (!normalized) {
412
- return;
462
+ continue;
413
463
  }
414
- const key = normalizeKey(normalized);
415
- if (seen.has(key)) {
416
- return;
464
+ const lower = normalized.toLowerCase();
465
+ if (!doseVariantMap.has(lower)) {
466
+ doseVariantMap.set(lower, { value: normalized, lower });
417
467
  }
418
- seen.add(key);
419
- suggestions.push(normalized);
468
+ }
469
+ const doseVariants = [...doseVariantMap.values()];
470
+ const push = (value, lower) => {
471
+ if (!lower) {
472
+ return false;
473
+ }
474
+ if (seen.has(lower)) {
475
+ return false;
476
+ }
477
+ if (!matcher(value, lower)) {
478
+ return false;
479
+ }
480
+ seen.add(lower);
481
+ suggestions.push(value);
482
+ return suggestions.length >= limit;
420
483
  };
421
484
  for (const pair of pairs) {
422
485
  const unitVariants = getUnitVariants(pair.unit);
486
+ const route = pair.route;
487
+ const routeLower = pair.routeLower;
423
488
  for (const code of FREQUENCY_CODES) {
489
+ const codeSuffix = ` ${code}`;
424
490
  for (const unitVariant of unitVariants) {
425
- for (const dose of doseValues) {
426
- push(`${dose} ${unitVariant} ${pair.route} ${code}`);
491
+ const unitRoute = `${unitVariant.value} ${route}`;
492
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
493
+ for (const doseVariant of doseVariants) {
494
+ const candidate = `${doseVariant.value} ${unitRoute}${codeSuffix}`;
495
+ const candidateLower = `${doseVariant.lower} ${unitRouteLower}${codeSuffix}`;
496
+ if (push(candidate, candidateLower)) {
497
+ return suggestions;
498
+ }
427
499
  }
428
500
  }
429
- push(`${pair.route} ${code}`);
501
+ const candidate = `${route}${codeSuffix}`;
502
+ const candidateLower = `${routeLower}${codeSuffix}`;
503
+ if (push(candidate, candidateLower)) {
504
+ return suggestions;
505
+ }
430
506
  }
431
507
  for (const interval of intervalTokens) {
508
+ const intervalSuffix = ` ${interval}`;
432
509
  for (const unitVariant of unitVariants) {
433
- for (const dose of doseValues) {
434
- push(`${dose} ${unitVariant} ${pair.route} ${interval}`);
510
+ const unitRoute = `${unitVariant.value} ${route}`;
511
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
512
+ for (const doseVariant of doseVariants) {
513
+ const base = `${doseVariant.value} ${unitRoute}`;
514
+ const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
515
+ const intervalCandidate = `${base}${intervalSuffix}`;
516
+ const intervalCandidateLower = `${baseLower}${intervalSuffix}`;
517
+ if (push(intervalCandidate, intervalCandidateLower)) {
518
+ return suggestions;
519
+ }
435
520
  for (const reason of prnReasons) {
436
- push(`${dose} ${unitVariant} ${pair.route} ${interval} prn ${reason}`);
521
+ const reasonSuffix = `${intervalSuffix} prn ${reason}`;
522
+ const reasonCandidate = `${base}${reasonSuffix}`;
523
+ const reasonCandidateLower = `${baseLower}${reasonSuffix}`;
524
+ if (push(reasonCandidate, reasonCandidateLower)) {
525
+ return suggestions;
526
+ }
437
527
  }
438
528
  }
439
529
  }
440
- push(`${pair.route} ${interval}`);
530
+ const candidate = `${route}${intervalSuffix}`;
531
+ const candidateLower = `${routeLower}${intervalSuffix}`;
532
+ if (push(candidate, candidateLower)) {
533
+ return suggestions;
534
+ }
441
535
  }
442
536
  for (const freq of FREQUENCY_NUMBERS) {
443
537
  const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
444
538
  if (!freqToken) {
445
539
  continue;
446
540
  }
447
- push(`1x${freq} ${pair.route} ${freqToken}`);
541
+ const base = `1x${freq} ${route}`;
542
+ const baseLower = `1x${freq} ${routeLower}`;
543
+ const freqCandidate = `${base} ${freqToken}`;
544
+ const freqCandidateLower = `${baseLower} ${freqToken}`;
545
+ if (push(freqCandidate, freqCandidateLower)) {
546
+ return suggestions;
547
+ }
448
548
  for (const when of CORE_WHEN_TOKENS) {
449
- push(`1x${freq} ${pair.route} ${when}`);
549
+ const whenCandidate = `${base} ${when}`;
550
+ const whenCandidateLower = `${baseLower} ${when}`;
551
+ if (push(whenCandidate, whenCandidateLower)) {
552
+ return suggestions;
553
+ }
450
554
  }
451
555
  }
452
556
  for (const whenSequence of whenSequences) {
453
- const suffix = whenSequence.join(" ");
557
+ const suffix = ` ${whenSequence.join(" ")}`;
454
558
  for (const unitVariant of unitVariants) {
455
- for (const dose of doseValues) {
456
- push(`${dose} ${unitVariant} ${pair.route} ${suffix}`);
559
+ const unitRoute = `${unitVariant.value} ${route}`;
560
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
561
+ for (const doseVariant of doseVariants) {
562
+ const base = `${doseVariant.value} ${unitRoute}`;
563
+ const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
564
+ const candidate = `${base}${suffix}`;
565
+ const candidateLower = `${baseLower}${suffix}`;
566
+ if (push(candidate, candidateLower)) {
567
+ return suggestions;
568
+ }
457
569
  }
458
570
  }
459
- push(`${pair.route} ${suffix}`);
571
+ const candidate = `${route}${suffix}`;
572
+ const candidateLower = `${routeLower}${suffix}`;
573
+ if (push(candidate, candidateLower)) {
574
+ return suggestions;
575
+ }
460
576
  }
461
577
  for (const reason of prnReasons) {
578
+ const reasonSuffix = ` prn ${reason}`;
462
579
  for (const unitVariant of unitVariants) {
463
- for (const dose of doseValues) {
464
- push(`${dose} ${unitVariant} ${pair.route} prn ${reason}`);
580
+ const unitRoute = `${unitVariant.value} ${route}`;
581
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
582
+ for (const doseVariant of doseVariants) {
583
+ const base = `${doseVariant.value} ${unitRoute}`;
584
+ const baseLower = `${doseVariant.lower} ${unitRouteLower}`;
585
+ const candidate = `${base}${reasonSuffix}`;
586
+ const candidateLower = `${baseLower}${reasonSuffix}`;
587
+ if (push(candidate, candidateLower)) {
588
+ return suggestions;
589
+ }
465
590
  }
466
591
  }
467
- push(`${pair.route} prn ${reason}`);
592
+ const candidate = `${route}${reasonSuffix}`;
593
+ const candidateLower = `${routeLower}${reasonSuffix}`;
594
+ if (push(candidate, candidateLower)) {
595
+ return suggestions;
596
+ }
468
597
  }
469
598
  }
470
599
  return suggestions;
471
600
  }
472
- function matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes) {
473
- if (!prefix) {
601
+ function matchesPrefix(_candidate, candidateLower, context) {
602
+ var _a, _b, _c, _d, _e, _f;
603
+ if (!context.raw) {
474
604
  return true;
475
605
  }
476
- const normalizedCandidate = candidate.toLowerCase();
477
- if (normalizedCandidate.startsWith(prefix)) {
606
+ if (!context.hasCanonical && !context.hasTokens) {
478
607
  return true;
479
608
  }
480
- const compactCandidate = normalizedCandidate.replace(/\s+/g, "");
481
- if (compactCandidate.startsWith(prefixCompact)) {
609
+ if (candidateLower.startsWith(context.raw)) {
482
610
  return true;
483
611
  }
484
- const candidateNoDashes = normalizedCandidate.replace(/-/g, "");
485
- if (candidateNoDashes.startsWith(prefixNoDashes)) {
486
- return true;
612
+ const fingerprint = getCandidateFingerprint(candidateLower);
613
+ if (context.requiresCompact) {
614
+ const compactCandidate = (_a = fingerprint.compact) !== null && _a !== void 0 ? _a : (fingerprint.compact = removeWhitespaceCharacters(candidateLower));
615
+ if (compactCandidate.startsWith(context.compact)) {
616
+ return true;
617
+ }
487
618
  }
488
- const canonicalCandidate = canonicalizeForMatching(candidate);
489
- if (canonicalCandidate.startsWith(prefixCanonical)) {
490
- return true;
619
+ if (context.requiresNoDashes) {
620
+ const candidateNoDashes = (_b = fingerprint.noDashes) !== null && _b !== void 0 ? _b : (fingerprint.noDashes = removeDashes(candidateLower));
621
+ if (candidateNoDashes.startsWith(context.noDashes)) {
622
+ return true;
623
+ }
491
624
  }
492
- const canonicalCompact = canonicalCandidate.replace(/\s+/g, "");
493
- if (canonicalCompact.startsWith(prefixCanonicalCompact)) {
494
- return true;
625
+ const getCandidateTokens = () => {
626
+ if (!fingerprint.tokens) {
627
+ fingerprint.tokens = tokenizeLowercaseForMatching(candidateLower);
628
+ }
629
+ return fingerprint.tokens;
630
+ };
631
+ if (context.hasCanonical) {
632
+ const canonicalCandidate = (_c = fingerprint.canonical) !== null && _c !== void 0 ? _c : (fingerprint.canonical = getCandidateTokens().join(" "));
633
+ if (canonicalCandidate.startsWith(context.canonical)) {
634
+ return true;
635
+ }
636
+ if (context.requiresCanonicalCompact) {
637
+ const canonicalCompact = (_d = fingerprint.canonicalCompact) !== null && _d !== void 0 ? _d : (fingerprint.canonicalCompact = removeWhitespaceCharacters(canonicalCandidate));
638
+ if (canonicalCompact.startsWith(context.canonicalCompact)) {
639
+ return true;
640
+ }
641
+ }
642
+ if (context.requiresCanonicalNoDashes) {
643
+ const canonicalNoDashes = (_e = fingerprint.canonicalNoDashes) !== null && _e !== void 0 ? _e : (fingerprint.canonicalNoDashes = removeDashes(canonicalCandidate));
644
+ if (canonicalNoDashes.startsWith(context.canonicalNoDashes)) {
645
+ return true;
646
+ }
647
+ }
495
648
  }
496
- const candidateTokens = tokenizeForMatching(candidate);
497
- if (tokensMatch(prefixTokens, candidateTokens)) {
498
- return true;
649
+ if (context.hasTokens) {
650
+ const resolvedTokens = getCandidateTokens();
651
+ if (tokensMatch(context.tokens, resolvedTokens)) {
652
+ return true;
653
+ }
654
+ if (context.requiresTokenNoDashes) {
655
+ const candidateTokensNoDashes = (_f = fingerprint.tokensNoDashes) !== null && _f !== void 0 ? _f : (fingerprint.tokensNoDashes = resolvedTokens.map((token) => removeDashes(token)));
656
+ if (tokensMatch(context.tokensNoDashes, candidateTokensNoDashes)) {
657
+ return true;
658
+ }
659
+ }
499
660
  }
500
- const canonicalNoDashes = canonicalCandidate.replace(/-/g, "");
501
- if (canonicalNoDashes.startsWith(prefixCanonicalNoDashes)) {
661
+ else if (context.requiresTokenNoDashes) {
502
662
  return true;
503
663
  }
504
- const candidateTokensNoDashes = candidateTokens.map((token) => token.replace(/-/g, ""));
505
- return tokensMatch(prefixTokensNoDashes, candidateTokensNoDashes);
664
+ return false;
506
665
  }
507
666
  function suggestSig(input, options) {
508
667
  var _a, _b;
509
668
  const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : DEFAULT_LIMIT;
669
+ if (limit <= 0) {
670
+ return [];
671
+ }
510
672
  const prefix = normalizeSpacing(input.toLowerCase());
511
673
  const prefixCompact = prefix.replace(/\s+/g, "");
512
674
  const prefixNoDashes = prefix.replace(/-/g, "");
513
- const prefixCanonical = canonicalizeForMatching(prefix);
675
+ const prefixTokens = tokenizeLowercaseForMatching(prefix);
676
+ const prefixCanonical = prefixTokens.join(" ");
514
677
  const prefixCanonicalCompact = prefixCanonical.replace(/\s+/g, "");
515
678
  const prefixCanonicalNoDashes = prefixCanonical.replace(/-/g, "");
516
- const prefixTokens = tokenizeForMatching(prefixCanonical);
517
679
  const prefixTokensNoDashes = prefixTokens.map((token) => token.replace(/-/g, ""));
680
+ const prefixContext = {
681
+ raw: prefix,
682
+ compact: prefixCompact,
683
+ noDashes: prefixNoDashes,
684
+ canonical: prefixCanonical,
685
+ canonicalCompact: prefixCanonicalCompact,
686
+ canonicalNoDashes: prefixCanonicalNoDashes,
687
+ tokens: prefixTokens,
688
+ tokensNoDashes: prefixTokensNoDashes,
689
+ hasCanonical: prefixCanonical.length > 0,
690
+ hasTokens: prefixTokens.length > 0,
691
+ requiresCompact: prefixCompact !== prefix,
692
+ requiresNoDashes: prefixNoDashes !== prefix,
693
+ requiresCanonicalCompact: prefixCanonicalCompact !== prefixCanonical,
694
+ requiresCanonicalNoDashes: prefixCanonicalNoDashes !== prefixCanonical,
695
+ requiresTokenNoDashes: prefixTokens.some((token, index) => token !== prefixTokensNoDashes[index]),
696
+ };
518
697
  const contextUnit = (0, context_1.inferUnitFromContext)((_b = options === null || options === void 0 ? void 0 : options.context) !== null && _b !== void 0 ? _b : undefined);
519
698
  const pairs = buildUnitRoutePairs(contextUnit, options);
520
699
  const doseValues = buildDoseValues(input);
521
700
  const prnReasons = buildPrnReasons(options === null || options === void 0 ? void 0 : options.prnReasons);
522
701
  const intervalTokens = buildIntervalTokens(input);
523
- const whenSequences = buildWhenSequences();
524
- const candidates = generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences);
525
- const results = [];
526
- for (const candidate of candidates) {
527
- if (matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes)) {
528
- results.push(candidate);
529
- }
530
- if (results.length >= limit) {
531
- break;
532
- }
533
- }
534
- return results;
702
+ const whenSequences = PRECOMPUTED_WHEN_SEQUENCES;
703
+ const matcher = (candidate, candidateLower) => matchesPrefix(candidate, candidateLower, prefixContext);
704
+ return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher);
535
705
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",