ezmedicationinput 0.1.26 → 0.1.28

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/format.js CHANGED
@@ -587,6 +587,7 @@ function formatLong(internal) {
587
587
  const frequencyPart = describeFrequency(internal);
588
588
  const eventParts = collectWhenPhrases(internal);
589
589
  if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
590
+ const timeStrings = [];
590
591
  for (const time of internal.timeOfDay) {
591
592
  const parts = time.split(":");
592
593
  const h = parseInt(parts[0], 10);
@@ -594,7 +595,10 @@ function formatLong(internal) {
594
595
  const isAm = h < 12;
595
596
  const displayH = h % 12 || 12;
596
597
  const displayM = m < 10 ? `0${m}` : `${m}`;
597
- eventParts.push(`at ${displayH}:${displayM}${isAm ? " am" : " pm"}`);
598
+ timeStrings.push(`${displayH}:${displayM}${isAm ? " am" : " pm"}`);
599
+ }
600
+ if (timeStrings.length > 0) {
601
+ eventParts.push(`at ${timeStrings.join(", ")}`);
598
602
  }
599
603
  }
600
604
  const timing = combineFrequencyAndEvents(frequencyPart, eventParts);
package/dist/i18n.js CHANGED
@@ -116,6 +116,16 @@ const WHEN_TEXT_THAI = {
116
116
  [types_1.EventTiming["After Sleep"]]: "หลังจากนอน",
117
117
  [types_1.EventTiming.Immediate]: "ทันที"
118
118
  };
119
+ const INSTRUCTION_TEXT_THAI = {
120
+ "Take with or after food": "รับประทานพร้อมหรือหลังอาหาร",
121
+ "With or after food": "พร้อมหรือหลังอาหาร",
122
+ "Take before food": "รับประทานก่อนอาหาร",
123
+ "Take on an empty stomach": "รับประทานขณะท้องว่าง",
124
+ "Take with plenty of water": "รับประทานพร้อมน้ำดื่มจำนวนมาก",
125
+ "Dissolve or mix with water before taking": "ละลายหรือผสมน้ำก่อนรับประทาน",
126
+ "Avoid alcoholic drinks": "หลีกเลี่ยงเครื่องดื่มแอลกอฮอล์",
127
+ "May cause drowsiness; do not drive if affected": "อาจทำให้ง่วงซึม; ห้ามขับขี่ยานพาหนะหรือทำงานกับเครื่องจักรหากมีอาการ",
128
+ };
119
129
  const DAY_NAMES_THAI = {
120
130
  mon: "วันจันทร์",
121
131
  tue: "วันอังคาร",
@@ -634,6 +644,7 @@ function formatAsNeededThai(internal) {
634
644
  return "ใช้เมื่อจำเป็น";
635
645
  }
636
646
  function formatShortThai(internal) {
647
+ var _a;
637
648
  const parts = [];
638
649
  const dose = formatDoseThaiShort(internal);
639
650
  if (dose) {
@@ -671,6 +682,10 @@ function formatShortThai(internal) {
671
682
  parts.push(events.join(" "));
672
683
  }
673
684
  }
685
+ if ((_a = internal.timeOfDay) === null || _a === void 0 ? void 0 : _a.length) {
686
+ const times = internal.timeOfDay.map((t) => t.slice(0, 5)).join(",");
687
+ parts.push(times);
688
+ }
674
689
  if (internal.dayOfWeek.length) {
675
690
  const days = internal.dayOfWeek
676
691
  .map((d) => { var _a, _b; return (_b = (_a = DAY_NAMES_THAI[d]) === null || _a === void 0 ? void 0 : _a.replace(/^วัน/, "")) !== null && _b !== void 0 ? _b : d; })
@@ -687,13 +702,27 @@ function formatShortThai(internal) {
687
702
  return parts.filter(Boolean).join(" ");
688
703
  }
689
704
  function formatLongThai(internal) {
690
- var _a;
705
+ var _a, _b;
691
706
  const grammar = resolveRouteGrammarThai(internal);
692
707
  const dosePart = (_a = formatDoseThaiLong(internal)) !== null && _a !== void 0 ? _a : "ยา";
693
708
  const sitePart = formatSiteThai(internal, grammar);
694
709
  const routePart = buildRoutePhraseThai(internal, grammar, Boolean(sitePart));
695
710
  const frequencyPart = describeFrequencyThai(internal);
696
711
  const eventParts = collectWhenPhrasesThai(internal);
712
+ if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
713
+ const timeStrings = [];
714
+ for (const time of internal.timeOfDay) {
715
+ const parts = time.split(":");
716
+ const h = parseInt(parts[0], 10);
717
+ const m = parseInt(parts[1], 10);
718
+ const displayM = m < 10 ? `0${m}` : `${m}`;
719
+ const displayH = h < 10 ? `0${h}` : `${h}`;
720
+ timeStrings.push(`${displayH}:${displayM}`);
721
+ }
722
+ if (timeStrings.length > 0) {
723
+ eventParts.push(`เวลา ${timeStrings.join(", ")}`);
724
+ }
725
+ }
697
726
  const timing = combineFrequencyAndEventsThai(frequencyPart, eventParts);
698
727
  const dayPart = describeDayOfWeekThai(internal);
699
728
  const countPart = internal.count !== undefined
@@ -724,9 +753,40 @@ function formatLongThai(internal) {
724
753
  }
725
754
  const body = segments.filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
726
755
  if (!body) {
727
- return `${grammar.verb}.`;
756
+ const instructionText = formatAdditionalInstructionsThai(internal);
757
+ if (!instructionText) {
758
+ return `${grammar.verb}.`;
759
+ }
760
+ return `${grammar.verb}. ${instructionText}`.trim();
761
+ }
762
+ const instructionText = formatAdditionalInstructionsThai(internal);
763
+ const baseSentence = `${grammar.verb} ${body}.`;
764
+ return instructionText ? `${baseSentence} ${instructionText}` : baseSentence;
765
+ }
766
+ function formatAdditionalInstructionsThai(internal) {
767
+ var _a;
768
+ if (!((_a = internal.additionalInstructions) === null || _a === void 0 ? void 0 : _a.length)) {
769
+ return undefined;
770
+ }
771
+ const phrases = internal.additionalInstructions
772
+ .map((instruction) => {
773
+ var _a;
774
+ const original = instruction.text || ((_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.display);
775
+ if (!original)
776
+ return undefined;
777
+ const normalized = original.trim();
778
+ return INSTRUCTION_TEXT_THAI[normalized] || normalized;
779
+ })
780
+ .filter((text) => Boolean(text))
781
+ .map((text) => text.trim())
782
+ .filter((text) => text.length > 0);
783
+ if (!phrases.length) {
784
+ return undefined;
728
785
  }
729
- return `${grammar.verb} ${body}.`;
786
+ return phrases
787
+ .map((phrase) => (/[.!?]$/.test(phrase) ? phrase : `${phrase}.`))
788
+ .join(" ")
789
+ .trim();
730
790
  }
731
791
  function stripTrailingZero(value) {
732
792
  const text = value.toString();
package/dist/maps.js CHANGED
@@ -673,6 +673,9 @@ exports.EVENT_TIMING_TOKENS = {
673
673
  breakfast: types_1.EventTiming.Breakfast,
674
674
  bfast: types_1.EventTiming.Breakfast,
675
675
  brkfst: types_1.EventTiming.Breakfast,
676
+ meal: types_1.EventTiming.Meal,
677
+ meals: types_1.EventTiming.Meal,
678
+ food: types_1.EventTiming.Meal,
676
679
  brk: types_1.EventTiming.Breakfast,
677
680
  cd: types_1.EventTiming.Lunch,
678
681
  lunch: types_1.EventTiming.Lunch,
@@ -717,6 +720,10 @@ registerMealKeywords(["dinner", "dinnertime", "supper", "suppertime"], {
717
720
  pc: types_1.EventTiming["After Dinner"],
718
721
  ac: types_1.EventTiming["Before Dinner"]
719
722
  });
723
+ registerMealKeywords(["meal", "meals", "food"], {
724
+ pc: types_1.EventTiming["After Meal"],
725
+ ac: types_1.EventTiming["Before Meal"]
726
+ });
720
727
  exports.MEAL_KEYWORDS = (0, object_1.objectFromEntries)(MEAL_KEYWORD_ENTRIES);
721
728
  exports.DISCOURAGED_TOKENS = {
722
729
  qd: "QD",
package/dist/parser.js CHANGED
@@ -781,15 +781,40 @@ function tryParseTimeBasedSchedule(internal, tokens, index) {
781
781
  const token = tokens[index];
782
782
  if (internal.consumed.has(token.index))
783
783
  return false;
784
- const isAtPrefix = token.lower === "@" || token.lower === "at";
785
- if (!isAtPrefix && !/^\d/.test(token.lower))
784
+ // Handle connectors like "and at" or just "and" before a time.
785
+ // This prevents rogue "and" from leaking into Additional Instructions
786
+ // when it serves as a connector between schedule parts.
787
+ let isAndPrefix = false;
788
+ let isAtPrefix = token.lower === "@" || token.lower === "at";
789
+ if (token.lower === "and" && !isAtPrefix) {
790
+ const next = tokens[index + 1];
791
+ if (next && !internal.consumed.has(next.index)) {
792
+ const nextLower = next.lower;
793
+ // If "and" is followed by "at", "@", or a number, it's a connector for this time block
794
+ if (nextLower === "@" || nextLower === "at" || /^\d/.test(nextLower)) {
795
+ isAndPrefix = true;
796
+ if (nextLower === "@" || nextLower === "at") {
797
+ isAtPrefix = true;
798
+ }
799
+ }
800
+ }
801
+ }
802
+ if (!isAtPrefix && !isAndPrefix && !/^\d/.test(token.lower))
786
803
  return false;
787
- let nextIndex = isAtPrefix ? index + 1 : index;
804
+ let nextIndex = index;
805
+ if (isAndPrefix)
806
+ nextIndex++;
807
+ if (isAtPrefix)
808
+ nextIndex++;
788
809
  const times = [];
789
810
  const consumedIndices = [];
790
811
  const timeTokens = [];
791
- if (isAtPrefix)
812
+ if (isAndPrefix)
792
813
  consumedIndices.push(index);
814
+ if (isAtPrefix) {
815
+ // If we have "and at", at is the second token (index + 1)
816
+ consumedIndices.push(isAndPrefix ? index + 1 : index);
817
+ }
793
818
  while (nextIndex < tokens.length) {
794
819
  const nextToken = tokens[nextIndex];
795
820
  if (!nextToken || internal.consumed.has(nextToken.index))
@@ -1657,7 +1682,7 @@ function applyCountLimit(internal, value) {
1657
1682
  return true;
1658
1683
  }
1659
1684
  function parseInternal(input, options) {
1660
- var _a, _b, _c, _d, _e, _f, _g;
1685
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1661
1686
  const tokens = tokenize(input);
1662
1687
  const internal = {
1663
1688
  input,
@@ -1695,15 +1720,20 @@ function parseInternal(input, options) {
1695
1720
  if (token.lower === "prn") {
1696
1721
  internal.asNeeded = true;
1697
1722
  mark(internal.consumed, token);
1698
- prnReasonStart = i + 1;
1723
+ let reasonIndex = i + 1;
1724
+ if (((_b = tokens[reasonIndex]) === null || _b === void 0 ? void 0 : _b.lower) === "for") {
1725
+ mark(internal.consumed, tokens[reasonIndex]);
1726
+ reasonIndex += 1;
1727
+ }
1728
+ prnReasonStart = reasonIndex;
1699
1729
  break;
1700
1730
  }
1701
- if (token.lower === "as" && ((_b = tokens[i + 1]) === null || _b === void 0 ? void 0 : _b.lower) === "needed") {
1731
+ if (token.lower === "as" && ((_c = tokens[i + 1]) === null || _c === void 0 ? void 0 : _c.lower) === "needed") {
1702
1732
  internal.asNeeded = true;
1703
1733
  mark(internal.consumed, token);
1704
1734
  mark(internal.consumed, tokens[i + 1]);
1705
1735
  let reasonIndex = i + 2;
1706
- if (((_c = tokens[reasonIndex]) === null || _c === void 0 ? void 0 : _c.lower) === "for") {
1736
+ if (((_d = tokens[reasonIndex]) === null || _d === void 0 ? void 0 : _d.lower) === "for") {
1707
1737
  mark(internal.consumed, tokens[reasonIndex]);
1708
1738
  reasonIndex += 1;
1709
1739
  }
@@ -1898,7 +1928,7 @@ function parseInternal(input, options) {
1898
1928
  // Frequency abbreviation map
1899
1929
  const freqDescriptor = normalizedLower === "od"
1900
1930
  ? undefined
1901
- : (_d = maps_1.TIMING_ABBREVIATIONS[token.lower]) !== null && _d !== void 0 ? _d : maps_1.TIMING_ABBREVIATIONS[normalizedLower];
1931
+ : (_e = maps_1.TIMING_ABBREVIATIONS[token.lower]) !== null && _e !== void 0 ? _e : maps_1.TIMING_ABBREVIATIONS[normalizedLower];
1902
1932
  if (freqDescriptor) {
1903
1933
  applyFrequencyDescriptor(internal, token, freqDescriptor, options);
1904
1934
  continue;
@@ -1913,34 +1943,32 @@ function parseInternal(input, options) {
1913
1943
  : types_1.EventTiming["Before Meal"]);
1914
1944
  continue;
1915
1945
  }
1916
- if (token.lower === "at" || token.lower === "@" || token.lower === "on") {
1946
+ if (token.lower === "at" || token.lower === "@" || token.lower === "on" || token.lower === "with") {
1917
1947
  if (parseAnchorSequence(internal, tokens, i)) {
1918
1948
  continue;
1919
1949
  }
1920
1950
  if (tryParseTimeBasedSchedule(internal, tokens, i)) {
1921
1951
  continue;
1922
1952
  }
1923
- mark(internal.consumed, token);
1924
- continue;
1953
+ // If none of the above consume it, and it's a known anchor prefix, mark it
1954
+ // but only if it's not "with" which might be part of other phrases later.
1955
+ if (token.lower !== "with") {
1956
+ mark(internal.consumed, token);
1957
+ continue;
1958
+ }
1925
1959
  }
1926
1960
  const nextToken = tokens[i + 1];
1927
1961
  if (nextToken && !internal.consumed.has(nextToken.index)) {
1928
1962
  const lowerNext = nextToken.lower;
1929
1963
  const combo = `${token.lower} ${lowerNext}`;
1930
- const comboWhen = (_e = COMBO_EVENT_TIMINGS[combo]) !== null && _e !== void 0 ? _e : maps_1.EVENT_TIMING_TOKENS[combo];
1964
+ const comboWhen = (_f = COMBO_EVENT_TIMINGS[combo]) !== null && _f !== void 0 ? _f : maps_1.EVENT_TIMING_TOKENS[combo];
1931
1965
  if (comboWhen) {
1932
1966
  applyWhenToken(internal, token, comboWhen);
1933
1967
  mark(internal.consumed, nextToken);
1934
1968
  continue;
1935
1969
  }
1936
- // Issue 2: Support "with meal" and "with food" combos explicitly if needed
1937
- if (token.lower === "with" && (lowerNext === "meal" || lowerNext === "food")) {
1938
- applyWhenToken(internal, token, types_1.EventTiming.Meal);
1939
- mark(internal.consumed, nextToken);
1940
- continue;
1941
- }
1942
1970
  }
1943
- const customWhen = (_f = options === null || options === void 0 ? void 0 : options.whenMap) === null || _f === void 0 ? void 0 : _f[token.lower];
1971
+ const customWhen = (_g = options === null || options === void 0 ? void 0 : options.whenMap) === null || _g === void 0 ? void 0 : _g[token.lower];
1944
1972
  if (customWhen) {
1945
1973
  applyWhenToken(internal, token, customWhen);
1946
1974
  continue;
@@ -2237,6 +2265,23 @@ function parseInternal(input, options) {
2237
2265
  // If it is a reclaimable connector, we can pull it back into the reason
2238
2266
  // if it helps form a coherent phrase like 'irritation at rectum'.
2239
2267
  }
2268
+ // If we haven't started collecting the reason yet, we should skip introductory
2269
+ // connectors to avoid phrases like "as needed for if pain".
2270
+ const PRN_INTRODUCTIONS = new Set(["for", "if", "when", "upon", "due", "to"]);
2271
+ if (reasonTokens.length === 0 && PRN_INTRODUCTIONS.has(token.lower)) {
2272
+ // Special handling for "due to" - if we skipped "due", we should also skip "to"
2273
+ if (token.lower === "due") {
2274
+ const next = tokens[i + 1];
2275
+ if (next && next.lower === "to") {
2276
+ mark(internal.consumed, token);
2277
+ mark(internal.consumed, next);
2278
+ i++; // skip next token in loop
2279
+ continue;
2280
+ }
2281
+ }
2282
+ mark(internal.consumed, token);
2283
+ continue;
2284
+ }
2240
2285
  reasonTokens.push(token.original);
2241
2286
  reasonIndices.push(token.index);
2242
2287
  reasonObjects.push(token);
@@ -2285,7 +2330,7 @@ function parseInternal(input, options) {
2285
2330
  let canonicalPrefix;
2286
2331
  if (reasonTokens.length > 0) {
2287
2332
  const suffixInfo = findTrailingPrnSiteSuffix(reasonObjects, internal, options);
2288
- if ((_g = suffixInfo === null || suffixInfo === void 0 ? void 0 : suffixInfo.tokens) === null || _g === void 0 ? void 0 : _g.length) {
2333
+ if ((_h = suffixInfo === null || suffixInfo === void 0 ? void 0 : suffixInfo.tokens) === null || _h === void 0 ? void 0 : _h.length) {
2289
2334
  for (const token of suffixInfo.tokens) {
2290
2335
  prnSiteSuffixIndices.add(token.index);
2291
2336
  }
@@ -2791,12 +2836,12 @@ function findAdditionalInstructionDefinition(text, canonical) {
2791
2836
  if (!entry.canonical) {
2792
2837
  continue;
2793
2838
  }
2839
+ // Check for exact canonical match first
2794
2840
  if (entry.canonical === canonical) {
2795
2841
  return entry.definition;
2796
2842
  }
2797
- if (canonical.includes(entry.canonical) || entry.canonical.includes(canonical)) {
2798
- return entry.definition;
2799
- }
2843
+ // Avoid broad includes checks (like "with" matching "with meal")
2844
+ // to prevent leakage of common connectors into additional instructions.
2800
2845
  for (const term of entry.terms) {
2801
2846
  const normalizedTerm = (0, maps_1.normalizeAdditionalInstructionKey)(term);
2802
2847
  if (!normalizedTerm) {
package/dist/suggest.js CHANGED
@@ -347,6 +347,43 @@ function canonicalizeLowercaseForMatching(value) {
347
347
  function canonicalizeForMatching(value) {
348
348
  return canonicalizeLowercaseForMatching(value.toLowerCase());
349
349
  }
350
+ function buildTimeTokens(input) {
351
+ const tokens = new Set();
352
+ // Add common times
353
+ for (let i = 1; i <= 12; i++) {
354
+ tokens.add(`at ${i}:00 am`);
355
+ tokens.add(`at ${i}:00 pm`);
356
+ }
357
+ // Analyze input for specific time requests to provide more granular suggestions
358
+ const match = input.match(/(?:at|@)\s*(\d{1,2})(?::(\d{0,2}))?/i);
359
+ if (match) {
360
+ const h = parseInt(match[1], 10);
361
+ if (h >= 1 && h <= 12) {
362
+ const m = match[2] || "00";
363
+ if (m.length === 1) {
364
+ tokens.add(`at ${h}:${m}0 am`);
365
+ tokens.add(`at ${h}:${m}0 pm`);
366
+ }
367
+ else {
368
+ tokens.add(`at ${h}:${m} am`);
369
+ tokens.add(`at ${h}:${m} pm`);
370
+ }
371
+ }
372
+ else if (h > 12 && h < 24) {
373
+ // Input seems to be 24h, but we format as am/pm usually.
374
+ // Let's add the 24h format as well if that's what they are typing?
375
+ // Or convert to am/pm? Let's add both for robustness.
376
+ const m = match[2] || "00";
377
+ if (m.length === 1) {
378
+ tokens.add(`at ${h}:${m}0`);
379
+ }
380
+ else {
381
+ tokens.add(`at ${h}:${m}`);
382
+ }
383
+ }
384
+ }
385
+ return [...tokens];
386
+ }
350
387
  function tokensMatch(prefixTokens, candidateTokens) {
351
388
  if (prefixTokens.length === 0) {
352
389
  return true;
@@ -461,7 +498,7 @@ function getCandidateFingerprint(candidateLower) {
461
498
  }
462
499
  return fingerprint;
463
500
  }
464
- function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher) {
501
+ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, timeTokens, whenSequences, limit, matcher) {
465
502
  const suggestions = [];
466
503
  const seen = new Set();
467
504
  const doseVariantMap = new Map();
@@ -502,6 +539,7 @@ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalToke
502
539
  const whenSuffixes = whenSequences === PRECOMPUTED_WHEN_SEQUENCES
503
540
  ? PRECOMPUTED_WHEN_SEQUENCE_SUFFIXES
504
541
  : whenSequences.map((sequence) => ` ${sequence.join(" ")}`);
542
+ const timeSuffixes = timeTokens.map((token) => ` ${token}`);
505
543
  for (let pairIndex = 0; pairIndex < pairs.length; pairIndex += 1) {
506
544
  const pair = pairs[pairIndex];
507
545
  const unitVariants = getUnitVariants(pair.unit);
@@ -593,6 +631,21 @@ function generateCandidateDirections(pairs, doseValues, prnReasons, intervalToke
593
631
  return suggestions;
594
632
  }
595
633
  }
634
+ for (let timeIndex = 0; timeIndex < timeSuffixes.length; timeIndex += 1) {
635
+ const timeSuffix = timeSuffixes[timeIndex];
636
+ for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
637
+ const doseBases = unitDoseVariants[unitIndex];
638
+ for (let doseIndex = 0; doseIndex < doseBases.length; doseIndex += 1) {
639
+ const base = doseBases[doseIndex];
640
+ if (push(base.value + timeSuffix, base.lower + timeSuffix)) {
641
+ return suggestions;
642
+ }
643
+ }
644
+ }
645
+ if (push(route + timeSuffix, routeLower + timeSuffix)) {
646
+ return suggestions;
647
+ }
648
+ }
596
649
  for (let reasonIndex = 0; reasonIndex < prnSuffixes.length; reasonIndex += 1) {
597
650
  const reasonSuffix = prnSuffixes[reasonIndex];
598
651
  for (let unitIndex = 0; unitIndex < unitDoseVariants.length; unitIndex += 1) {
@@ -712,7 +765,8 @@ function suggestSig(input, options) {
712
765
  const doseValues = buildDoseValues(input);
713
766
  const prnReasons = buildPrnReasons(options === null || options === void 0 ? void 0 : options.prnReasons);
714
767
  const intervalTokens = buildIntervalTokens(input);
768
+ const timeTokens = buildTimeTokens(input);
715
769
  const whenSequences = PRECOMPUTED_WHEN_SEQUENCES;
716
770
  const matcher = (candidate, candidateLower) => matchesPrefix(candidate, candidateLower, prefixContext);
717
- return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, whenSequences, limit, matcher);
771
+ return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, timeTokens, whenSequences, limit, matcher);
718
772
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",