ezmedicationinput 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/dist/suggest.js +230 -68
  2. package/package.json +1 -1
package/dist/suggest.js CHANGED
@@ -105,6 +105,7 @@ const WHEN_COMBINATIONS = [
105
105
  ].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
106
106
  const CORE_WHEN_TOKENS = ["pc", "ac", "hs"].filter((token) => maps_1.EVENT_TIMING_TOKENS[token] !== undefined);
107
107
  const FREQUENCY_CODES = ["qd", "od", "bid", "tid", "qid"].filter((token) => maps_1.TIMING_ABBREVIATIONS[token] !== undefined);
108
+ const FREQUENCY_CODE_SUFFIXES = FREQUENCY_CODES.map((code) => ` ${code}`);
108
109
  const FREQ_TOKEN_BY_NUMBER = {};
109
110
  for (const [frequency, token] of [
110
111
  [1, "qd"],
@@ -206,11 +207,45 @@ function normalizeSpacing(value) {
206
207
  .trim()
207
208
  .replace(/\s+/g, " ");
208
209
  }
210
+ function removeWhitespaceCharacters(value) {
211
+ for (let index = 0; index < value.length; index += 1) {
212
+ const code = value.charCodeAt(index);
213
+ if (code <= 32) {
214
+ const result = [];
215
+ for (let inner = 0; inner < value.length; inner += 1) {
216
+ const currentCode = value.charCodeAt(inner);
217
+ if (currentCode > 32) {
218
+ result.push(value.charAt(inner));
219
+ }
220
+ }
221
+ return result.join("");
222
+ }
223
+ }
224
+ return value;
225
+ }
226
+ function removeDashes(value) {
227
+ if (value.indexOf("-") === -1) {
228
+ return value;
229
+ }
230
+ const result = [];
231
+ for (let index = 0; index < value.length; index += 1) {
232
+ const char = value.charAt(index);
233
+ if (char !== "-") {
234
+ result.push(char);
235
+ }
236
+ }
237
+ return result.join("");
238
+ }
239
+ const UNIT_VARIANT_CACHE = new Map();
209
240
  function getUnitVariants(unit) {
210
241
  var _a;
211
242
  const canonical = (_a = resolveCanonicalUnit(unit)) !== null && _a !== void 0 ? _a : normalizeSpacing(unit);
212
243
  const normalizedCanonical = normalizeKey(canonical);
213
- const variants = new Set();
244
+ const cached = UNIT_VARIANT_CACHE.get(normalizedCanonical);
245
+ if (cached) {
246
+ return cached;
247
+ }
248
+ const variants = new Map();
214
249
  const push = (candidate) => {
215
250
  if (!candidate) {
216
251
  return;
@@ -219,7 +254,11 @@ function getUnitVariants(unit) {
219
254
  if (!normalizedCandidate) {
220
255
  return;
221
256
  }
222
- variants.add(normalizedCandidate);
257
+ const lower = normalizedCandidate.toLowerCase();
258
+ if (variants.has(lower)) {
259
+ return;
260
+ }
261
+ variants.set(lower, { value: normalizedCandidate, lower });
223
262
  };
224
263
  push(canonical);
225
264
  push(unit);
@@ -229,7 +268,9 @@ function getUnitVariants(unit) {
229
268
  push(candidate);
230
269
  }
231
270
  }
232
- return [...variants];
271
+ const result = [...variants.values()];
272
+ UNIT_VARIANT_CACHE.set(normalizedCanonical, result);
273
+ return result;
233
274
  }
234
275
  function buildIntervalTokens(input) {
235
276
  const intervals = new Set();
@@ -289,16 +330,22 @@ function buildWhenSequences() {
289
330
  return sequences;
290
331
  }
291
332
  const PRECOMPUTED_WHEN_SEQUENCES = buildWhenSequences();
292
- function tokenizeForMatching(value) {
333
+ const PRECOMPUTED_WHEN_SEQUENCE_SUFFIXES = PRECOMPUTED_WHEN_SEQUENCES.map((sequence) => ` ${sequence.join(" ")}`);
334
+ function tokenizeLowercaseForMatching(value) {
293
335
  return value
294
- .toLowerCase()
295
336
  .split(/\s+/)
296
337
  .map((token) => token.replace(/^[^a-z0-9-]+|[^a-z0-9-]+$/g, ""))
297
338
  .filter((token) => token.length > 0)
298
339
  .filter((token) => !OPTIONAL_MATCH_TOKENS.has(token));
299
340
  }
341
+ function tokenizeForMatching(value) {
342
+ return tokenizeLowercaseForMatching(value.toLowerCase());
343
+ }
344
+ function canonicalizeLowercaseForMatching(value) {
345
+ return tokenizeLowercaseForMatching(value).join(" ");
346
+ }
300
347
  function canonicalizeForMatching(value) {
301
- return tokenizeForMatching(value).join(" ");
348
+ return canonicalizeLowercaseForMatching(value.toLowerCase());
302
349
  }
303
350
  function tokensMatch(prefixTokens, candidateTokens) {
304
351
  if (prefixTokens.length === 0) {
@@ -343,12 +390,13 @@ function buildUnitRoutePairs(contextUnit, options) {
343
390
  if (!cleanRoute) {
344
391
  return;
345
392
  }
346
- const key = `${normalizedUnit}::${normalizeKey(cleanRoute)}`;
393
+ const routeLower = cleanRoute.toLowerCase();
394
+ const key = `${normalizedUnit}::${routeLower}`;
347
395
  if (seen.has(key)) {
348
396
  return;
349
397
  }
350
398
  seen.add(key);
351
- pairs.push({ unit: canonicalUnit, route: cleanRoute });
399
+ pairs.push({ unit: canonicalUnit, route: cleanRoute, routeLower });
352
400
  };
353
401
  addPair(contextUnit);
354
402
  for (const preference of DEFAULT_UNIT_ROUTE_ORDER) {
@@ -404,132 +452,229 @@ function buildDoseValues(input) {
404
452
  }
405
453
  return [...values];
406
454
  }
455
+ const CANDIDATE_FINGERPRINT_CACHE = new Map();
456
+ function getCandidateFingerprint(candidateLower) {
457
+ let fingerprint = CANDIDATE_FINGERPRINT_CACHE.get(candidateLower);
458
+ if (!fingerprint) {
459
+ fingerprint = {};
460
+ CANDIDATE_FINGERPRINT_CACHE.set(candidateLower, fingerprint);
461
+ }
462
+ return fingerprint;
463
+ }
407
464
  function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher) {
408
465
  const suggestions = [];
409
466
  const seen = new Set();
410
- const push = (value) => {
411
- const normalized = normalizeSpacing(value);
467
+ const doseVariantMap = new Map();
468
+ for (const dose of doseValues) {
469
+ const normalized = normalizeSpacing(dose);
412
470
  if (!normalized) {
471
+ continue;
472
+ }
473
+ const lower = normalized.toLowerCase();
474
+ if (!doseVariantMap.has(lower)) {
475
+ doseVariantMap.set(lower, { value: normalized, lower });
476
+ }
477
+ }
478
+ const doseVariants = [...doseVariantMap.values()];
479
+ const push = (value, lower) => {
480
+ if (!lower) {
413
481
  return false;
414
482
  }
415
- const key = normalizeKey(normalized);
416
- if (seen.has(key)) {
483
+ if (seen.has(lower)) {
417
484
  return false;
418
485
  }
419
- seen.add(key);
420
- if (!matcher(normalized)) {
486
+ if (!matcher(value, lower)) {
421
487
  return false;
422
488
  }
423
- suggestions.push(normalized);
489
+ seen.add(lower);
490
+ suggestions.push(value);
424
491
  return suggestions.length >= limit;
425
492
  };
426
- for (const pair of pairs) {
493
+ const codeSuffixes = FREQUENCY_CODE_SUFFIXES;
494
+ const prnSuffixes = new Array(prnReasons.length);
495
+ for (let i = 0; i < prnReasons.length; i += 1) {
496
+ prnSuffixes[i] = ` prn ${prnReasons[i]}`;
497
+ }
498
+ const intervalSuffixes = new Array(intervalTokens.length);
499
+ for (let i = 0; i < intervalTokens.length; i += 1) {
500
+ intervalSuffixes[i] = ` ${intervalTokens[i]}`;
501
+ }
502
+ const whenSuffixes = whenSequences === PRECOMPUTED_WHEN_SEQUENCES
503
+ ? PRECOMPUTED_WHEN_SEQUENCE_SUFFIXES
504
+ : whenSequences.map((sequence) => ` ${sequence.join(" ")}`);
505
+ for (let pairIndex = 0; pairIndex < pairs.length; pairIndex += 1) {
506
+ const pair = pairs[pairIndex];
427
507
  const unitVariants = getUnitVariants(pair.unit);
428
- for (const code of FREQUENCY_CODES) {
429
- for (const unitVariant of unitVariants) {
430
- for (const dose of doseValues) {
431
- if (push(`${dose} ${unitVariant} ${pair.route} ${code}`)) {
508
+ const route = pair.route;
509
+ const routeLower = pair.routeLower;
510
+ const unitDoseVariants = new Array(unitVariants.length);
511
+ for (let unitIndex = 0; unitIndex < unitVariants.length; unitIndex += 1) {
512
+ const unitVariant = unitVariants[unitIndex];
513
+ const unitRouteValue = `${unitVariant.value} ${route}`;
514
+ const unitRouteLower = `${unitVariant.lower} ${routeLower}`;
515
+ const doseBases = new Array(doseVariants.length);
516
+ for (let doseIndex = 0; doseIndex < doseVariants.length; doseIndex += 1) {
517
+ const doseVariant = doseVariants[doseIndex];
518
+ doseBases[doseIndex] = {
519
+ value: `${doseVariant.value} ${unitRouteValue}`,
520
+ lower: `${doseVariant.lower} ${unitRouteLower}`,
521
+ };
522
+ }
523
+ unitDoseVariants[unitIndex] = doseBases;
524
+ }
525
+ for (let codeIndex = 0; codeIndex < codeSuffixes.length; codeIndex += 1) {
526
+ const codeSuffix = codeSuffixes[codeIndex];
527
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
528
+ const doseBases = unitDoseVariants[unitIndex];
529
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
530
+ const base = doseBases[doseIndex];
531
+ if (push(base.value + codeSuffix, base.lower + codeSuffix)) {
432
532
  return suggestions;
433
533
  }
434
534
  }
435
535
  }
436
- if (push(`${pair.route} ${code}`)) {
536
+ if (push(route + codeSuffix, routeLower + codeSuffix)) {
437
537
  return suggestions;
438
538
  }
439
539
  }
440
- for (const interval of intervalTokens) {
441
- for (const unitVariant of unitVariants) {
442
- for (const dose of doseValues) {
443
- if (push(`${dose} ${unitVariant} ${pair.route} ${interval}`)) {
540
+ for (let intervalIndex = 0; intervalIndex < intervalSuffixes.length; intervalIndex += 1) {
541
+ const intervalSuffix = intervalSuffixes[intervalIndex];
542
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
543
+ const doseBases = unitDoseVariants[unitIndex];
544
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
545
+ const base = doseBases[doseIndex];
546
+ const baseIntervalValue = base.value + intervalSuffix;
547
+ const baseIntervalLower = base.lower + intervalSuffix;
548
+ if (push(baseIntervalValue, baseIntervalLower)) {
444
549
  return suggestions;
445
550
  }
446
- for (const reason of prnReasons) {
447
- if (push(`${dose} ${unitVariant} ${pair.route} ${interval} prn ${reason}`)) {
551
+ for (let reasonIndex = 0; reasonIndex < prnSuffixes.length; reasonIndex += 1) {
552
+ const reasonSuffix = prnSuffixes[reasonIndex];
553
+ if (push(baseIntervalValue + reasonSuffix, baseIntervalLower + reasonSuffix)) {
448
554
  return suggestions;
449
555
  }
450
556
  }
451
557
  }
452
558
  }
453
- if (push(`${pair.route} ${interval}`)) {
559
+ if (push(route + intervalSuffix, routeLower + intervalSuffix)) {
454
560
  return suggestions;
455
561
  }
456
562
  }
457
- for (const freq of FREQUENCY_NUMBERS) {
563
+ for (let freqIndex = 0; freqIndex < FREQUENCY_NUMBERS.length; freqIndex += 1) {
564
+ const freq = FREQUENCY_NUMBERS[freqIndex];
458
565
  const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
459
566
  if (!freqToken) {
460
567
  continue;
461
568
  }
462
- if (push(`1x${freq} ${pair.route} ${freqToken}`)) {
569
+ const baseValue = `1x${freq} ${route}`;
570
+ const baseLower = `1x${freq} ${routeLower}`;
571
+ if (push(`${baseValue} ${freqToken}`, `${baseLower} ${freqToken}`)) {
463
572
  return suggestions;
464
573
  }
465
- for (const when of CORE_WHEN_TOKENS) {
466
- if (push(`1x${freq} ${pair.route} ${when}`)) {
574
+ for (let whenIndex = 0; whenIndex < CORE_WHEN_TOKENS.length; whenIndex += 1) {
575
+ const whenToken = CORE_WHEN_TOKENS[whenIndex];
576
+ if (push(`${baseValue} ${whenToken}`, `${baseLower} ${whenToken}`)) {
467
577
  return suggestions;
468
578
  }
469
579
  }
470
580
  }
471
- for (const whenSequence of whenSequences) {
472
- const suffix = whenSequence.join(" ");
473
- for (const unitVariant of unitVariants) {
474
- for (const dose of doseValues) {
475
- if (push(`${dose} ${unitVariant} ${pair.route} ${suffix}`)) {
581
+ for (let whenIndex = 0; whenIndex < whenSuffixes.length; whenIndex += 1) {
582
+ const whenSuffix = whenSuffixes[whenIndex];
583
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
584
+ const doseBases = unitDoseVariants[unitIndex];
585
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
586
+ const base = doseBases[doseIndex];
587
+ if (push(base.value + whenSuffix, base.lower + whenSuffix)) {
476
588
  return suggestions;
477
589
  }
478
590
  }
479
591
  }
480
- if (push(`${pair.route} ${suffix}`)) {
592
+ if (push(route + whenSuffix, routeLower + whenSuffix)) {
481
593
  return suggestions;
482
594
  }
483
595
  }
484
- for (const reason of prnReasons) {
485
- for (const unitVariant of unitVariants) {
486
- for (const dose of doseValues) {
487
- if (push(`${dose} ${unitVariant} ${pair.route} prn ${reason}`)) {
596
+ for (let reasonIndex = 0; reasonIndex < prnSuffixes.length; reasonIndex += 1) {
597
+ const reasonSuffix = prnSuffixes[reasonIndex];
598
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
599
+ const doseBases = unitDoseVariants[unitIndex];
600
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
601
+ const base = doseBases[doseIndex];
602
+ if (push(base.value + reasonSuffix, base.lower + reasonSuffix)) {
488
603
  return suggestions;
489
604
  }
490
605
  }
491
606
  }
492
- if (push(`${pair.route} prn ${reason}`)) {
607
+ if (push(route + reasonSuffix, routeLower + reasonSuffix)) {
493
608
  return suggestions;
494
609
  }
495
610
  }
496
611
  }
497
612
  return suggestions;
498
613
  }
499
- function matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes) {
500
- if (!prefix) {
614
+ function matchesPrefix(_candidate, candidateLower, context) {
615
+ var _a, _b, _c, _d, _e, _f;
616
+ if (!context.raw) {
501
617
  return true;
502
618
  }
503
- const normalizedCandidate = candidate.toLowerCase();
504
- if (normalizedCandidate.startsWith(prefix)) {
619
+ if (!context.hasCanonical && !context.hasTokens) {
505
620
  return true;
506
621
  }
507
- const compactCandidate = normalizedCandidate.replace(/\s+/g, "");
508
- if (compactCandidate.startsWith(prefixCompact)) {
622
+ if (candidateLower.startsWith(context.raw)) {
509
623
  return true;
510
624
  }
511
- const candidateNoDashes = normalizedCandidate.replace(/-/g, "");
512
- if (candidateNoDashes.startsWith(prefixNoDashes)) {
513
- return true;
625
+ const fingerprint = getCandidateFingerprint(candidateLower);
626
+ if (context.requiresCompact) {
627
+ const compactCandidate = (_a = fingerprint.compact) !== null && _a !== void 0 ? _a : (fingerprint.compact = removeWhitespaceCharacters(candidateLower));
628
+ if (compactCandidate.startsWith(context.compact)) {
629
+ return true;
630
+ }
514
631
  }
515
- const canonicalCandidate = canonicalizeForMatching(candidate);
516
- if (canonicalCandidate.startsWith(prefixCanonical)) {
517
- return true;
632
+ if (context.requiresNoDashes) {
633
+ const candidateNoDashes = (_b = fingerprint.noDashes) !== null && _b !== void 0 ? _b : (fingerprint.noDashes = removeDashes(candidateLower));
634
+ if (candidateNoDashes.startsWith(context.noDashes)) {
635
+ return true;
636
+ }
518
637
  }
519
- const canonicalCompact = canonicalCandidate.replace(/\s+/g, "");
520
- if (canonicalCompact.startsWith(prefixCanonicalCompact)) {
521
- return true;
638
+ const getCandidateTokens = () => {
639
+ if (!fingerprint.tokens) {
640
+ fingerprint.tokens = tokenizeLowercaseForMatching(candidateLower);
641
+ }
642
+ return fingerprint.tokens;
643
+ };
644
+ if (context.hasCanonical) {
645
+ const canonicalCandidate = (_c = fingerprint.canonical) !== null && _c !== void 0 ? _c : (fingerprint.canonical = getCandidateTokens().join(" "));
646
+ if (canonicalCandidate.startsWith(context.canonical)) {
647
+ return true;
648
+ }
649
+ if (context.requiresCanonicalCompact) {
650
+ const canonicalCompact = (_d = fingerprint.canonicalCompact) !== null && _d !== void 0 ? _d : (fingerprint.canonicalCompact = removeWhitespaceCharacters(canonicalCandidate));
651
+ if (canonicalCompact.startsWith(context.canonicalCompact)) {
652
+ return true;
653
+ }
654
+ }
655
+ if (context.requiresCanonicalNoDashes) {
656
+ const canonicalNoDashes = (_e = fingerprint.canonicalNoDashes) !== null && _e !== void 0 ? _e : (fingerprint.canonicalNoDashes = removeDashes(canonicalCandidate));
657
+ if (canonicalNoDashes.startsWith(context.canonicalNoDashes)) {
658
+ return true;
659
+ }
660
+ }
522
661
  }
523
- const candidateTokens = tokenizeForMatching(candidate);
524
- if (tokensMatch(prefixTokens, candidateTokens)) {
525
- return true;
662
+ if (context.hasTokens) {
663
+ const resolvedTokens = getCandidateTokens();
664
+ if (tokensMatch(context.tokens, resolvedTokens)) {
665
+ return true;
666
+ }
667
+ if (context.requiresTokenNoDashes) {
668
+ const candidateTokensNoDashes = (_f = fingerprint.tokensNoDashes) !== null && _f !== void 0 ? _f : (fingerprint.tokensNoDashes = resolvedTokens.map((token) => removeDashes(token)));
669
+ if (tokensMatch(context.tokensNoDashes, candidateTokensNoDashes)) {
670
+ return true;
671
+ }
672
+ }
526
673
  }
527
- const canonicalNoDashes = canonicalCandidate.replace(/-/g, "");
528
- if (canonicalNoDashes.startsWith(prefixCanonicalNoDashes)) {
674
+ else if (context.requiresTokenNoDashes) {
529
675
  return true;
530
676
  }
531
- const candidateTokensNoDashes = candidateTokens.map((token) => token.replace(/-/g, ""));
532
- return tokensMatch(prefixTokensNoDashes, candidateTokensNoDashes);
677
+ return false;
533
678
  }
534
679
  function suggestSig(input, options) {
535
680
  var _a, _b;
@@ -540,17 +685,34 @@ function suggestSig(input, options) {
540
685
  const prefix = normalizeSpacing(input.toLowerCase());
541
686
  const prefixCompact = prefix.replace(/\s+/g, "");
542
687
  const prefixNoDashes = prefix.replace(/-/g, "");
543
- const prefixCanonical = canonicalizeForMatching(prefix);
688
+ const prefixTokens = tokenizeLowercaseForMatching(prefix);
689
+ const prefixCanonical = prefixTokens.join(" ");
544
690
  const prefixCanonicalCompact = prefixCanonical.replace(/\s+/g, "");
545
691
  const prefixCanonicalNoDashes = prefixCanonical.replace(/-/g, "");
546
- const prefixTokens = tokenizeForMatching(prefixCanonical);
547
692
  const prefixTokensNoDashes = prefixTokens.map((token) => token.replace(/-/g, ""));
693
+ const prefixContext = {
694
+ raw: prefix,
695
+ compact: prefixCompact,
696
+ noDashes: prefixNoDashes,
697
+ canonical: prefixCanonical,
698
+ canonicalCompact: prefixCanonicalCompact,
699
+ canonicalNoDashes: prefixCanonicalNoDashes,
700
+ tokens: prefixTokens,
701
+ tokensNoDashes: prefixTokensNoDashes,
702
+ hasCanonical: prefixCanonical.length > 0,
703
+ hasTokens: prefixTokens.length > 0,
704
+ requiresCompact: prefixCompact !== prefix,
705
+ requiresNoDashes: prefixNoDashes !== prefix,
706
+ requiresCanonicalCompact: prefixCanonicalCompact !== prefixCanonical,
707
+ requiresCanonicalNoDashes: prefixCanonicalNoDashes !== prefixCanonical,
708
+ requiresTokenNoDashes: prefixTokens.some((token, index) => token !== prefixTokensNoDashes[index]),
709
+ };
548
710
  const contextUnit = (0, context_1.inferUnitFromContext)((_b = options === null || options === void 0 ? void 0 : options.context) !== null && _b !== void 0 ? _b : undefined);
549
711
  const pairs = buildUnitRoutePairs(contextUnit, options);
550
712
  const doseValues = buildDoseValues(input);
551
713
  const prnReasons = buildPrnReasons(options === null || options === void 0 ? void 0 : options.prnReasons);
552
714
  const intervalTokens = buildIntervalTokens(input);
553
715
  const whenSequences = PRECOMPUTED_WHEN_SEQUENCES;
554
- const matcher = (candidate) => matchesPrefix(candidate, prefix, prefixCompact, prefixTokens, prefixTokensNoDashes, prefixCanonical, prefixCanonicalCompact, prefixNoDashes, prefixCanonicalNoDashes);
716
+ const matcher = (candidate, candidateLower) => matchesPrefix(candidate, candidateLower, prefixContext);
555
717
  return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher);
556
718
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",