ezmedicationinput 0.1.27 → 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))
@@ -1918,15 +1943,19 @@ function parseInternal(input, options) {
1918
1943
  : types_1.EventTiming["Before Meal"]);
1919
1944
  continue;
1920
1945
  }
1921
- if (token.lower === "at" || token.lower === "@" || token.lower === "on") {
1946
+ if (token.lower === "at" || token.lower === "@" || token.lower === "on" || token.lower === "with") {
1922
1947
  if (parseAnchorSequence(internal, tokens, i)) {
1923
1948
  continue;
1924
1949
  }
1925
1950
  if (tryParseTimeBasedSchedule(internal, tokens, i)) {
1926
1951
  continue;
1927
1952
  }
1928
- mark(internal.consumed, token);
1929
- 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
+ }
1930
1959
  }
1931
1960
  const nextToken = tokens[i + 1];
1932
1961
  if (nextToken && !internal.consumed.has(nextToken.index)) {
@@ -1938,12 +1967,6 @@ function parseInternal(input, options) {
1938
1967
  mark(internal.consumed, nextToken);
1939
1968
  continue;
1940
1969
  }
1941
- // Issue 2: Support "with meal" and "with food" combos explicitly if needed
1942
- if (token.lower === "with" && (lowerNext === "meal" || lowerNext === "food")) {
1943
- applyWhenToken(internal, token, types_1.EventTiming.Meal);
1944
- mark(internal.consumed, nextToken);
1945
- continue;
1946
- }
1947
1970
  }
1948
1971
  const customWhen = (_g = options === null || options === void 0 ? void 0 : options.whenMap) === null || _g === void 0 ? void 0 : _g[token.lower];
1949
1972
  if (customWhen) {
@@ -2813,12 +2836,12 @@ function findAdditionalInstructionDefinition(text, canonical) {
2813
2836
  if (!entry.canonical) {
2814
2837
  continue;
2815
2838
  }
2839
+ // Check for exact canonical match first
2816
2840
  if (entry.canonical === canonical) {
2817
2841
  return entry.definition;
2818
2842
  }
2819
- if (canonical.includes(entry.canonical) || entry.canonical.includes(canonical)) {
2820
- return entry.definition;
2821
- }
2843
+ // Avoid broad includes checks (like "with" matching "with meal")
2844
+ // to prevent leakage of common connectors into additional instructions.
2822
2845
  for (const term of entry.terms) {
2823
2846
  const normalizedTerm = (0, maps_1.normalizeAdditionalInstructionKey)(term);
2824
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.27",
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",