ezmedicationinput 0.1.17 → 0.1.19

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/parser.js +143 -19
  2. package/package.json +1 -1
package/dist/parser.js CHANGED
@@ -1445,6 +1445,7 @@ function parseInternal(input, options) {
1445
1445
  }
1446
1446
  // PRN detection
1447
1447
  let prnReasonStart;
1448
+ const prnSiteSuffixIndices = new Set();
1448
1449
  for (let i = 0; i < tokens.length; i++) {
1449
1450
  const token = tokens[i];
1450
1451
  if (token.lower === "prn") {
@@ -1529,6 +1530,9 @@ function parseInternal(input, options) {
1529
1530
  };
1530
1531
  // Process tokens sequentially
1531
1532
  const tryRouteSynonym = (startIndex) => {
1533
+ if (prnReasonStart !== undefined && startIndex >= prnReasonStart) {
1534
+ return false;
1535
+ }
1532
1536
  const maxSpan = Math.min(24, tokens.length - startIndex);
1533
1537
  for (let span = maxSpan; span >= 1; span--) {
1534
1538
  const slice = tokens.slice(startIndex, startIndex + span);
@@ -1901,7 +1905,7 @@ function parseInternal(input, options) {
1901
1905
  for (let i = prnReasonStart; i < tokens.length; i++) {
1902
1906
  const token = tokens[i];
1903
1907
  if (internal.consumed.has(token.index)) {
1904
- continue;
1908
+ internal.consumed.delete(token.index);
1905
1909
  }
1906
1910
  reasonTokens.push(token.original);
1907
1911
  reasonIndices.push(token.index);
@@ -1948,6 +1952,14 @@ function parseInternal(input, options) {
1948
1952
  }
1949
1953
  }
1950
1954
  }
1955
+ if (reasonTokens.length > 0) {
1956
+ const suffixTokens = findTrailingPrnSiteSuffix(reasonObjects, internal, options);
1957
+ if (suffixTokens === null || suffixTokens === void 0 ? void 0 : suffixTokens.length) {
1958
+ for (const token of suffixTokens) {
1959
+ prnSiteSuffixIndices.add(token.index);
1960
+ }
1961
+ }
1962
+ }
1951
1963
  if (reasonTokens.length > 0) {
1952
1964
  const joined = reasonTokens.join(" ").trim();
1953
1965
  if (joined) {
@@ -1983,21 +1995,31 @@ function parseInternal(input, options) {
1983
1995
  // Determine site text from leftover tokens (excluding PRN reason tokens)
1984
1996
  const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
1985
1997
  const siteCandidateIndices = new Set();
1998
+ const leftoverSiteIndices = new Set();
1986
1999
  for (const token of leftoverTokens) {
2000
+ if (prnSiteSuffixIndices.has(token.index)) {
2001
+ continue;
2002
+ }
1987
2003
  const normalized = normalizeTokenLower(token);
1988
2004
  if (isBodySiteHint(normalized, internal.customSiteHints)) {
1989
2005
  siteCandidateIndices.add(token.index);
2006
+ leftoverSiteIndices.add(token.index);
1990
2007
  continue;
1991
2008
  }
1992
2009
  if (SITE_CONNECTORS.has(normalized)) {
1993
2010
  const next = tokens[token.index + 1];
1994
- if (next && !internal.consumed.has(next.index)) {
2011
+ if (next && !internal.consumed.has(next.index) && !prnSiteSuffixIndices.has(next.index)) {
1995
2012
  siteCandidateIndices.add(next.index);
1996
2013
  }
1997
2014
  }
1998
2015
  }
1999
- for (const idx of internal.siteTokenIndices) {
2000
- siteCandidateIndices.add(idx);
2016
+ if (leftoverSiteIndices.size === 0) {
2017
+ for (const idx of internal.siteTokenIndices) {
2018
+ if (prnSiteSuffixIndices.has(idx)) {
2019
+ continue;
2020
+ }
2021
+ siteCandidateIndices.add(idx);
2022
+ }
2001
2023
  }
2002
2024
  if (siteCandidateIndices.size > 0) {
2003
2025
  const indicesToInclude = new Set(siteCandidateIndices);
@@ -2492,18 +2514,52 @@ function normalizeSiteDisplayText(text, customSiteMap) {
2492
2514
  return trimmed;
2493
2515
  }
2494
2516
  const canonicalInput = (0, maps_1.normalizeBodySiteKey)(trimmed);
2495
- if (!canonicalInput || !isAdjectivalSitePhrase(canonicalInput)) {
2496
- return trimmed;
2497
- }
2498
- const definition = (_a = lookupBodySiteDefinition(customSiteMap, canonicalInput)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonicalInput];
2499
- if (!definition) {
2517
+ if (!canonicalInput) {
2500
2518
  return trimmed;
2501
2519
  }
2502
- const preferred = pickPreferredBodySitePhrase(canonicalInput, definition, customSiteMap);
2503
- if (!preferred) {
2504
- return trimmed;
2520
+ const resolvePreferred = (canonical) => {
2521
+ var _a;
2522
+ const definition = (_a = lookupBodySiteDefinition(customSiteMap, canonical)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonical];
2523
+ if (!definition) {
2524
+ return undefined;
2525
+ }
2526
+ const preferred = pickPreferredBodySitePhrase(canonical, definition, customSiteMap);
2527
+ const textValue = preferred !== null && preferred !== void 0 ? preferred : canonical;
2528
+ const normalized = (0, maps_1.normalizeBodySiteKey)(textValue);
2529
+ if (!normalized) {
2530
+ return undefined;
2531
+ }
2532
+ return { text: textValue, canonical: normalized };
2533
+ };
2534
+ if (isAdjectivalSitePhrase(canonicalInput)) {
2535
+ const direct = resolvePreferred(canonicalInput);
2536
+ return (_a = direct === null || direct === void 0 ? void 0 : direct.text) !== null && _a !== void 0 ? _a : trimmed;
2537
+ }
2538
+ const words = canonicalInput.split(/\s+/).filter((word) => word.length > 0);
2539
+ for (let i = 1; i < words.length; i++) {
2540
+ const prefix = words.slice(0, i);
2541
+ if (!prefix.every((word) => isAdjectivalSitePhrase(word))) {
2542
+ continue;
2543
+ }
2544
+ const candidateCanonical = words.slice(i).join(" ");
2545
+ if (!candidateCanonical) {
2546
+ continue;
2547
+ }
2548
+ const candidatePreferred = resolvePreferred(candidateCanonical);
2549
+ if (!candidatePreferred) {
2550
+ continue;
2551
+ }
2552
+ const prefixMatches = prefix.every((word) => {
2553
+ const normalizedPrefix = resolvePreferred(word);
2554
+ return (normalizedPrefix !== undefined &&
2555
+ normalizedPrefix.canonical === candidatePreferred.canonical);
2556
+ });
2557
+ if (!prefixMatches) {
2558
+ continue;
2559
+ }
2560
+ return candidatePreferred.text;
2505
2561
  }
2506
- return preferred;
2562
+ return trimmed;
2507
2563
  }
2508
2564
  function pickPreferredBodySitePhrase(canonical, definition, customSiteMap) {
2509
2565
  const synonyms = new Set();
@@ -2532,12 +2588,12 @@ function pickPreferredBodySitePhrase(canonical, definition, customSiteMap) {
2532
2588
  if (normalizedKey) {
2533
2589
  synonyms.add(normalizedKey);
2534
2590
  }
2535
- }
2536
- if (candidate.aliases) {
2537
- for (const alias of candidate.aliases) {
2538
- const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
2539
- if (normalizedAlias) {
2540
- synonyms.add(normalizedAlias);
2591
+ if (candidate.aliases) {
2592
+ for (const alias of candidate.aliases) {
2593
+ const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
2594
+ if (normalizedAlias) {
2595
+ synonyms.add(normalizedAlias);
2596
+ }
2541
2597
  }
2542
2598
  }
2543
2599
  }
@@ -2810,6 +2866,74 @@ function findPrnReasonSeparator(sourceText) {
2810
2866
  }
2811
2867
  return undefined;
2812
2868
  }
2869
+ function findTrailingPrnSiteSuffix(tokens, internal, options) {
2870
+ var _a;
2871
+ let suffixStart;
2872
+ let hasSiteHint = false;
2873
+ let hasConnector = false;
2874
+ for (let i = tokens.length - 1; i >= 0; i--) {
2875
+ const token = tokens[i];
2876
+ const lower = normalizeTokenLower(token);
2877
+ if (!lower) {
2878
+ if (suffixStart !== undefined && token.original.trim()) {
2879
+ break;
2880
+ }
2881
+ continue;
2882
+ }
2883
+ if (isBodySiteHint(lower, internal.customSiteHints)) {
2884
+ hasSiteHint = true;
2885
+ suffixStart = i;
2886
+ continue;
2887
+ }
2888
+ if (suffixStart !== undefined) {
2889
+ if (SITE_CONNECTORS.has(lower)) {
2890
+ hasConnector = true;
2891
+ suffixStart = i;
2892
+ continue;
2893
+ }
2894
+ if (SITE_FILLER_WORDS.has(lower) || ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
2895
+ suffixStart = i;
2896
+ continue;
2897
+ }
2898
+ }
2899
+ if (suffixStart !== undefined) {
2900
+ break;
2901
+ }
2902
+ }
2903
+ if (!hasSiteHint || !hasConnector || suffixStart === undefined || suffixStart === 0) {
2904
+ return undefined;
2905
+ }
2906
+ const suffixTokens = tokens.slice(suffixStart);
2907
+ const siteWords = [];
2908
+ const siteHintTokens = [];
2909
+ for (const token of suffixTokens) {
2910
+ const trimmed = token.original.trim();
2911
+ if (!trimmed) {
2912
+ continue;
2913
+ }
2914
+ const lower = normalizeTokenLower(token);
2915
+ if (SITE_CONNECTORS.has(lower) ||
2916
+ SITE_FILLER_WORDS.has(lower) ||
2917
+ ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
2918
+ continue;
2919
+ }
2920
+ siteHintTokens.push(token);
2921
+ siteWords.push(trimmed);
2922
+ }
2923
+ if (!siteWords.length) {
2924
+ return undefined;
2925
+ }
2926
+ const sitePhrase = siteWords.join(" ");
2927
+ const canonical = (0, maps_1.normalizeBodySiteKey)(sitePhrase);
2928
+ if (!canonical) {
2929
+ return undefined;
2930
+ }
2931
+ 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];
2932
+ if (!definition) {
2933
+ return undefined;
2934
+ }
2935
+ return siteHintTokens;
2936
+ }
2813
2937
  function lookupPrnReasonDefinition(map, canonical) {
2814
2938
  if (!map) {
2815
2939
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",