ezmedicationinput 0.1.27 → 0.1.29

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
@@ -625,15 +625,18 @@ function describeDayOfWeekThai(internal) {
625
625
  return `ใน${joinWithAndThai(days)}`;
626
626
  }
627
627
  function formatAsNeededThai(internal) {
628
+ var _a, _b;
628
629
  if (!internal.asNeeded) {
629
630
  return undefined;
630
631
  }
631
632
  if (internal.asNeededReason) {
632
- return `ใช้เมื่อจำเป็นสำหรับ ${internal.asNeededReason}`;
633
+ const translation = (_b = (_a = internal.asNeededReasonCoding) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.th;
634
+ return `ใช้เมื่อจำเป็นสำหรับ ${translation || internal.asNeededReason}`;
633
635
  }
634
636
  return "ใช้เมื่อจำเป็น";
635
637
  }
636
638
  function formatShortThai(internal) {
639
+ var _a;
637
640
  const parts = [];
638
641
  const dose = formatDoseThaiShort(internal);
639
642
  if (dose) {
@@ -671,6 +674,10 @@ function formatShortThai(internal) {
671
674
  parts.push(events.join(" "));
672
675
  }
673
676
  }
677
+ if ((_a = internal.timeOfDay) === null || _a === void 0 ? void 0 : _a.length) {
678
+ const times = internal.timeOfDay.map((t) => t.slice(0, 5)).join(",");
679
+ parts.push(times);
680
+ }
674
681
  if (internal.dayOfWeek.length) {
675
682
  const days = internal.dayOfWeek
676
683
  .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 +694,27 @@ function formatShortThai(internal) {
687
694
  return parts.filter(Boolean).join(" ");
688
695
  }
689
696
  function formatLongThai(internal) {
690
- var _a;
697
+ var _a, _b;
691
698
  const grammar = resolveRouteGrammarThai(internal);
692
699
  const dosePart = (_a = formatDoseThaiLong(internal)) !== null && _a !== void 0 ? _a : "ยา";
693
700
  const sitePart = formatSiteThai(internal, grammar);
694
701
  const routePart = buildRoutePhraseThai(internal, grammar, Boolean(sitePart));
695
702
  const frequencyPart = describeFrequencyThai(internal);
696
703
  const eventParts = collectWhenPhrasesThai(internal);
704
+ if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
705
+ const timeStrings = [];
706
+ for (const time of internal.timeOfDay) {
707
+ const parts = time.split(":");
708
+ const h = parseInt(parts[0], 10);
709
+ const m = parseInt(parts[1], 10);
710
+ const displayM = m < 10 ? `0${m}` : `${m}`;
711
+ const displayH = h < 10 ? `0${h}` : `${h}`;
712
+ timeStrings.push(`${displayH}:${displayM}`);
713
+ }
714
+ if (timeStrings.length > 0) {
715
+ eventParts.push(`เวลา ${timeStrings.join(", ")}`);
716
+ }
717
+ }
697
718
  const timing = combineFrequencyAndEventsThai(frequencyPart, eventParts);
698
719
  const dayPart = describeDayOfWeekThai(internal);
699
720
  const countPart = internal.count !== undefined
@@ -724,9 +745,42 @@ function formatLongThai(internal) {
724
745
  }
725
746
  const body = segments.filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
726
747
  if (!body) {
727
- return `${grammar.verb}.`;
748
+ const instructionText = formatAdditionalInstructionsThai(internal);
749
+ if (!instructionText) {
750
+ return `${grammar.verb}.`;
751
+ }
752
+ return `${grammar.verb}. ${instructionText}`.trim();
753
+ }
754
+ const instructionText = formatAdditionalInstructionsThai(internal);
755
+ const baseSentence = `${grammar.verb} ${body}.`;
756
+ return instructionText ? `${baseSentence} ${instructionText}` : baseSentence;
757
+ }
758
+ function formatAdditionalInstructionsThai(internal) {
759
+ var _a;
760
+ if (!((_a = internal.additionalInstructions) === null || _a === void 0 ? void 0 : _a.length)) {
761
+ return undefined;
762
+ }
763
+ const phrases = internal.additionalInstructions
764
+ .map((instruction) => {
765
+ var _a, _b, _c;
766
+ const translation = (_b = (_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.i18n) === null || _b === void 0 ? void 0 : _b.th;
767
+ if (translation)
768
+ return translation;
769
+ const original = instruction.text || ((_c = instruction.coding) === null || _c === void 0 ? void 0 : _c.display);
770
+ if (!original)
771
+ return undefined;
772
+ return original.trim();
773
+ })
774
+ .filter((text) => Boolean(text))
775
+ .map((text) => text.trim())
776
+ .filter((text) => text.length > 0);
777
+ if (!phrases.length) {
778
+ return undefined;
728
779
  }
729
- return `${grammar.verb} ${body}.`;
780
+ return phrases
781
+ .map((phrase) => (/[.!?]$/.test(phrase) ? phrase : `${phrase}.`))
782
+ .join(" ")
783
+ .trim();
730
784
  }
731
785
  function stripTrailingZero(value) {
732
786
  const text = value.toString();
@@ -36,12 +36,16 @@ export interface ParsedSigInternal {
36
36
  timingCode?: string;
37
37
  asNeeded?: boolean;
38
38
  asNeededReason?: string;
39
- asNeededReasonCoding?: FhirCoding;
39
+ asNeededReasonCoding?: FhirCoding & {
40
+ i18n?: Record<string, string>;
41
+ };
40
42
  warnings: string[];
41
43
  siteText?: string;
42
44
  siteSource?: "abbreviation" | "text";
43
45
  siteTokenIndices: Set<number>;
44
- siteCoding?: FhirCoding;
46
+ siteCoding?: FhirCoding & {
47
+ i18n?: Record<string, string>;
48
+ };
45
49
  siteLookupRequest?: SiteCodeLookupRequest;
46
50
  siteLookups: SiteLookupDetail[];
47
51
  customSiteHints?: Set<string>;
@@ -49,6 +53,8 @@ export interface ParsedSigInternal {
49
53
  prnReasonLookups: PrnReasonLookupDetail[];
50
54
  additionalInstructions: Array<{
51
55
  text?: string;
52
- coding?: FhirCoding;
56
+ coding?: FhirCoding & {
57
+ i18n?: Record<string, string>;
58
+ };
53
59
  }>;
54
60
  }
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",
@@ -1281,14 +1288,16 @@ const DEFAULT_PRN_REASON_SOURCE = [
1281
1288
  names: ["pain", "ache", "aches", "pains"],
1282
1289
  definition: {
1283
1290
  coding: { system: SNOMED_SYSTEM, code: "22253000", display: "Pain" },
1284
- text: "Pain"
1291
+ text: "Pain",
1292
+ i18n: { th: "ปวด" }
1285
1293
  }
1286
1294
  },
1287
1295
  {
1288
1296
  names: ["nausea", "queasiness", "vomiting", "n/v", "nausea and vomiting"],
1289
1297
  definition: {
1290
1298
  coding: { system: SNOMED_SYSTEM, code: "422587007", display: "Nausea" },
1291
- text: "Nausea"
1299
+ text: "Nausea",
1300
+ i18n: { th: "คลื่นไส้" }
1292
1301
  }
1293
1302
  },
1294
1303
  {
@@ -1299,14 +1308,16 @@ const DEFAULT_PRN_REASON_SOURCE = [
1299
1308
  code: "418363000",
1300
1309
  display: "Itching of skin"
1301
1310
  },
1302
- text: "Itching"
1311
+ text: "Itching",
1312
+ i18n: { th: "คัน" }
1303
1313
  }
1304
1314
  },
1305
1315
  {
1306
1316
  names: ["anxiety", "nervousness"],
1307
1317
  definition: {
1308
1318
  coding: { system: SNOMED_SYSTEM, code: "48694002", display: "Anxiety" },
1309
- text: "Anxiety"
1319
+ text: "Anxiety",
1320
+ i18n: { th: "วิตกกังวล" }
1310
1321
  }
1311
1322
  },
1312
1323
  {
@@ -1317,28 +1328,32 @@ const DEFAULT_PRN_REASON_SOURCE = [
1317
1328
  code: "193462001",
1318
1329
  display: "Insomnia"
1319
1330
  },
1320
- text: "Sleep"
1331
+ text: "Sleep",
1332
+ i18n: { th: "นอนหลับ" }
1321
1333
  }
1322
1334
  },
1323
1335
  {
1324
1336
  names: ["cough", "coughing"],
1325
1337
  definition: {
1326
1338
  coding: { system: SNOMED_SYSTEM, code: "49727002", display: "Cough" },
1327
- text: "Cough"
1339
+ text: "Cough",
1340
+ i18n: { th: "ไอ" }
1328
1341
  }
1329
1342
  },
1330
1343
  {
1331
1344
  names: ["fever", "temperature", "pyrexia"],
1332
1345
  definition: {
1333
1346
  coding: { system: SNOMED_SYSTEM, code: "386661006", display: "Fever" },
1334
- text: "Fever"
1347
+ text: "Fever",
1348
+ i18n: { th: "ไข้" }
1335
1349
  }
1336
1350
  },
1337
1351
  {
1338
1352
  names: ["spasm", "spasms", "muscle spasm"],
1339
1353
  definition: {
1340
1354
  coding: { system: SNOMED_SYSTEM, code: "45352006", display: "Spasm" },
1341
- text: "Spasm"
1355
+ text: "Spasm",
1356
+ i18n: { th: "ตะคริวหรือเกร็ง" }
1342
1357
  }
1343
1358
  },
1344
1359
  {
@@ -1349,7 +1364,8 @@ const DEFAULT_PRN_REASON_SOURCE = [
1349
1364
  code: "14760008",
1350
1365
  display: "Constipation"
1351
1366
  },
1352
- text: "Constipation"
1367
+ text: "Constipation",
1368
+ i18n: { th: "ท้องผูก" }
1353
1369
  }
1354
1370
  },
1355
1371
  {
@@ -1360,7 +1376,8 @@ const DEFAULT_PRN_REASON_SOURCE = [
1360
1376
  code: "257553007",
1361
1377
  display: "Irritation"
1362
1378
  },
1363
- text: "Irritation"
1379
+ text: "Irritation",
1380
+ i18n: { th: "ระคายเคือง" }
1364
1381
  }
1365
1382
  },
1366
1383
  {
@@ -1371,7 +1388,8 @@ const DEFAULT_PRN_REASON_SOURCE = [
1371
1388
  code: "267036007",
1372
1389
  display: "Dyspnea"
1373
1390
  },
1374
- text: "Shortness of breath"
1391
+ text: "Shortness of breath",
1392
+ i18n: { th: "เหนื่อยหรือหายใจลำบาก" }
1375
1393
  }
1376
1394
  }
1377
1395
  ];
@@ -1412,7 +1430,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1412
1430
  code: "311504000",
1413
1431
  display: "With or after food"
1414
1432
  },
1415
- text: "Take with or after food"
1433
+ text: "Take with or after food",
1434
+ i18n: { th: "รับประทานพร้อมหรือหลังอาหาร" }
1416
1435
  }
1417
1436
  },
1418
1437
  {
@@ -1423,7 +1442,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1423
1442
  code: "311501008",
1424
1443
  display: "Half to one hour before food"
1425
1444
  },
1426
- text: "Take before food"
1445
+ text: "Take before food",
1446
+ i18n: { th: "รับประทานก่อนอาหาร" }
1427
1447
  }
1428
1448
  },
1429
1449
  {
@@ -1434,7 +1454,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1434
1454
  code: "717154004",
1435
1455
  display: "Take on an empty stomach (qualifier value)"
1436
1456
  },
1437
- text: "Take on an empty stomach"
1457
+ text: "Take on an empty stomach",
1458
+ i18n: { th: "รับประทานขณะท้องว่าง" }
1438
1459
  }
1439
1460
  },
1440
1461
  {
@@ -1445,7 +1466,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1445
1466
  code: "419303009",
1446
1467
  display: "With plenty of water"
1447
1468
  },
1448
- text: "Take with plenty of water"
1469
+ text: "Take with plenty of water",
1470
+ i18n: { th: "รับประทานพร้อมน้ำดื่มจำนวนมาก" }
1449
1471
  }
1450
1472
  },
1451
1473
  {
@@ -1456,7 +1478,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1456
1478
  code: "417995008",
1457
1479
  display: "Dissolve or mix with water before taking"
1458
1480
  },
1459
- text: "Dissolve or mix with water before taking"
1481
+ text: "Dissolve or mix with water before taking",
1482
+ i18n: { th: "ละลายหรือผสมน้ำก่อนรับประทาน" }
1460
1483
  }
1461
1484
  },
1462
1485
  {
@@ -1467,7 +1490,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1467
1490
  code: "419822006",
1468
1491
  display: "Warning. Avoid alcoholic drink (qualifier value)"
1469
1492
  },
1470
- text: "Avoid alcoholic drinks"
1493
+ text: "Avoid alcoholic drinks",
1494
+ i18n: { th: "หลีกเลี่ยงเครื่องดื่มแอลกอฮอล์" }
1471
1495
  }
1472
1496
  },
1473
1497
  {
@@ -1478,7 +1502,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1478
1502
  code: "418954008",
1479
1503
  display: "Warning. May cause drowsiness. If affected do not drive or operate machinery (qualifier value)"
1480
1504
  },
1481
- text: "May cause drowsiness; do not drive if affected"
1505
+ text: "May cause drowsiness; do not drive if affected",
1506
+ i18n: { th: "อาจทำให้ง่วงซึม; ห้ามขับขี่ยานพาหนะหรือทำงานกับเครื่องจักรหากมีอาการ" }
1482
1507
  }
1483
1508
  },
1484
1509
  {
@@ -1499,7 +1524,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1499
1524
  code: "418914006",
1500
1525
  display: "Warning. May cause drowsiness. If affected do not drive or operate machinery. Avoid alcoholic drink (qualifier value)"
1501
1526
  },
1502
- text: "May cause drowsiness; avoid driving or alcohol"
1527
+ text: "May cause drowsiness; avoid driving or alcohol",
1528
+ i18n: { th: "อาจทำให้ง่วงซึม; หลีกเลี่ยงการขับขี่ยานพาหนะหรือดื่มแอลกอฮอล์" }
1503
1529
  }
1504
1530
  },
1505
1531
  {
@@ -1516,7 +1542,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1516
1542
  code: "418071006",
1517
1543
  display: "Warning. Causes drowsiness which may continue the next day. If affected do not drive or operate machinery. Avoid alcoholic drink (qualifier value)"
1518
1544
  },
1519
- text: "May cause next-day drowsiness; avoid driving or alcohol"
1545
+ text: "May cause next-day drowsiness; avoid driving or alcohol",
1546
+ i18n: { th: "อาจทำให้ง่วงซึมในวันถัดมา; หลีกเลี่ยงการขับขี่ยานพาหนะหรือดื่มแอลกอฮอล์" }
1520
1547
  }
1521
1548
  },
1522
1549
  {
@@ -1527,7 +1554,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1527
1554
  code: "418521000",
1528
1555
  display: "Avoid exposure of skin to direct sunlight or sun lamps (qualifier value)"
1529
1556
  },
1530
- text: "Avoid sunlight or sun lamps"
1557
+ text: "Avoid sunlight or sun lamps",
1558
+ i18n: { th: "หลีกเลี่ยงแสงแดดหรือหลอดไฟแสงยูวี" }
1531
1559
  }
1532
1560
  },
1533
1561
  {
@@ -1538,7 +1566,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1538
1566
  code: "418693002",
1539
1567
  display: "Swallowed whole, not chewed (qualifier value)"
1540
1568
  },
1541
- text: "Swallow whole; do not crush or chew"
1569
+ text: "Swallow whole; do not crush or chew",
1570
+ i18n: { th: "กลืนทั้งเม็ด; ห้ามเคี้ยวหรือบด" }
1542
1571
  }
1543
1572
  },
1544
1573
  {
@@ -1549,7 +1578,8 @@ const DEFAULT_ADDITIONAL_INSTRUCTION_SOURCE = [
1549
1578
  code: "418991002",
1550
1579
  display: "Sucked or chewed (qualifier value)"
1551
1580
  },
1552
- text: "Suck or chew before swallowing"
1581
+ text: "Suck or chew before swallowing",
1582
+ i18n: { th: "เคี้ยวหรืออมให้ละลายก่อนกลืน" }
1553
1583
  }
1554
1584
  }
1555
1585
  ];
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) {
@@ -3145,7 +3168,8 @@ function collectAdditionalInstructions(internal, tokens) {
3145
3168
  ? {
3146
3169
  code: definition.coding.code,
3147
3170
  display: definition.coding.display,
3148
- system: (_f = definition.coding.system) !== null && _f !== void 0 ? _f : SNOMED_SYSTEM
3171
+ system: (_f = definition.coding.system) !== null && _f !== void 0 ? _f : SNOMED_SYSTEM,
3172
+ i18n: definition.i18n
3149
3173
  }
3150
3174
  : undefined
3151
3175
  });
@@ -3375,7 +3399,8 @@ function applyPrnReasonDefinition(internal, definition) {
3375
3399
  ? {
3376
3400
  code: coding.code,
3377
3401
  display: coding.display,
3378
- system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
3402
+ system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM,
3403
+ i18n: definition.i18n
3379
3404
  }
3380
3405
  : undefined;
3381
3406
  if (definition.text && !internal.asNeededReason) {
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/dist/types.d.ts CHANGED
@@ -316,6 +316,8 @@ export interface CodeableConceptDefinition {
316
316
  coding?: FhirCoding;
317
317
  text?: string;
318
318
  aliases?: string[];
319
+ /** Optional translations for different locales (e.g., { "th": "ปวด" }) */
320
+ i18n?: Record<string, string>;
319
321
  }
320
322
  export interface PrnReasonDefinition extends CodeableConceptDefinition {
321
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",