ezmedicationinput 0.1.36 → 0.1.38

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/README.md CHANGED
@@ -346,6 +346,10 @@ You can specify the number of times (total count) the medication is supposed to
346
346
  combinations (e.g. `1x3` → breakfast/lunch/dinner). This also respects
347
347
  `context.mealRelation` when provided and only applies to schedules with four
348
348
  or fewer daily doses.
349
+ - `enableMealDashSyntax`: when `true`, enables shorthand meal-dose patterns
350
+ such as `1-0-1`, `1-0-1 pc`, `10-12-0 ac`, and `1-0-0-1 ac`. The parser
351
+ expands them into multiple dosage clauses aligned to breakfast/lunch/dinner
352
+ (plus bedtime for a 4th slot).
349
353
  - `twoPerDayPair`: controls whether 2× AC/PC/C doses expand to breakfast+dinner (default) or breakfast+lunch.
350
354
  - `assumeSingleDiscreteDose`: when `true`, missing discrete doses (such as
351
355
  tablets or capsules) default to a single unit when the parser can infer a
package/dist/i18n.js CHANGED
@@ -551,8 +551,25 @@ function combineFrequencyAndEventsThai(frequency, events) {
551
551
  }
552
552
  return { frequency, event: joinWithAndThai(events) };
553
553
  }
554
+ function isOralRouteThai(internal) {
555
+ var _a;
556
+ if (internal.routeCode === types_1.RouteCode["Oral route"]) {
557
+ return true;
558
+ }
559
+ const text = (_a = internal.routeText) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
560
+ if (!text) {
561
+ return false;
562
+ }
563
+ return (text === "po" ||
564
+ text === "oral" ||
565
+ text.includes("mouth") ||
566
+ text.includes("per os"));
567
+ }
554
568
  function buildRoutePhraseThai(internal, grammar, hasSite) {
555
569
  var _a;
570
+ if (grammar.verb === "รับประทาน" && isOralRouteThai(internal)) {
571
+ return undefined;
572
+ }
556
573
  if (typeof grammar.routePhrase === "function") {
557
574
  return grammar.routePhrase({ hasSite, internal });
558
575
  }
package/dist/index.js CHANGED
@@ -58,6 +58,96 @@ Object.defineProperty(exports, "DEFAULT_BODY_SITE_SNOMED_SOURCE", { enumerable:
58
58
  Object.defineProperty(exports, "DEFAULT_ROUTE_SYNONYMS", { enumerable: true, get: function () { return maps_1.DEFAULT_ROUTE_SYNONYMS; } });
59
59
  Object.defineProperty(exports, "DEFAULT_UNIT_BY_ROUTE", { enumerable: true, get: function () { return maps_1.DEFAULT_UNIT_BY_ROUTE; } });
60
60
  Object.defineProperty(exports, "KNOWN_DOSAGE_FORMS_TO_DOSE", { enumerable: true, get: function () { return maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE; } });
61
+ function parseMealDashValues(token) {
62
+ if (!/^[0-9]+(?:\.[0-9]+)?(?:-[0-9]+(?:\.[0-9]+)?){2,3}$/.test(token)) {
63
+ return undefined;
64
+ }
65
+ const values = token.split("-").map((part) => Number(part));
66
+ if (values.length !== 3 && values.length !== 4) {
67
+ return undefined;
68
+ }
69
+ if (!values.every((value) => Number.isFinite(value) && value >= 0)) {
70
+ return undefined;
71
+ }
72
+ return values;
73
+ }
74
+ function mealDashEvents(length, relation) {
75
+ const base = relation === "ac"
76
+ ? ["ACM", "ACD", "ACV"]
77
+ : relation === "pc"
78
+ ? ["PCM", "PCD", "PCV"]
79
+ : ["CM", "CD", "CV"];
80
+ if (length === 4) {
81
+ return [...base, "HS"];
82
+ }
83
+ return base;
84
+ }
85
+ function formatMealDashAmount(value) {
86
+ if (Number.isInteger(value)) {
87
+ return String(value);
88
+ }
89
+ return String(value).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
90
+ }
91
+ function expandMealDashSegment(segment) {
92
+ const tokens = (0, parser_1.tokenize)(segment.text);
93
+ if (tokens.length === 0) {
94
+ return [segment];
95
+ }
96
+ const firstToken = tokens[0];
97
+ const values = parseMealDashValues(firstToken.lower);
98
+ if (!values) {
99
+ return [segment];
100
+ }
101
+ let relation = "meal";
102
+ let relationIndex = -1;
103
+ for (let i = 1; i < tokens.length; i += 1) {
104
+ const lower = tokens[i].lower.replace(/[.,;:]/g, "");
105
+ if (lower === "ac") {
106
+ relation = "ac";
107
+ relationIndex = i;
108
+ break;
109
+ }
110
+ if (lower === "pc") {
111
+ relation = "pc";
112
+ relationIndex = i;
113
+ break;
114
+ }
115
+ }
116
+ const suffixTokens = tokens
117
+ .filter((token, index) => index !== 0 && index !== relationIndex)
118
+ .map((token) => token.original);
119
+ const events = mealDashEvents(values.length, relation);
120
+ const expanded = values
121
+ .map((value, index) => ({ value, event: events[index] }))
122
+ .filter(({ value }) => value > 0)
123
+ .map(({ value, event }) => {
124
+ const text = [formatMealDashAmount(value), ...suffixTokens, event]
125
+ .filter((part) => part && part.trim().length > 0)
126
+ .join(" ")
127
+ .replace(/\s+/g, " ")
128
+ .trim();
129
+ return {
130
+ text,
131
+ start: segment.start,
132
+ end: segment.end
133
+ };
134
+ })
135
+ .filter((item) => item.text.length > 0);
136
+ if (expanded.length === 0) {
137
+ return [segment];
138
+ }
139
+ return expanded;
140
+ }
141
+ function expandMealDashSegments(segments, options) {
142
+ if (!(options === null || options === void 0 ? void 0 : options.enableMealDashSyntax)) {
143
+ return segments;
144
+ }
145
+ const expanded = [];
146
+ for (const segment of segments) {
147
+ expanded.push(...expandMealDashSegment(segment));
148
+ }
149
+ return expanded;
150
+ }
61
151
  function toSegmentMeta(segments) {
62
152
  return segments.map((segment, index) => ({
63
153
  index,
@@ -66,7 +156,7 @@ function toSegmentMeta(segments) {
66
156
  }));
67
157
  }
68
158
  function parseSig(input, options) {
69
- const segments = (0, segment_1.splitSigSegments)(input);
159
+ const segments = expandMealDashSegments((0, segment_1.splitSigSegments)(input), options);
70
160
  const carry = {};
71
161
  const results = [];
72
162
  for (const segment of segments) {
@@ -92,7 +182,7 @@ function parseSig(input, options) {
92
182
  };
93
183
  }
94
184
  function lintSig(input, options) {
95
- const segments = (0, segment_1.splitSigSegments)(input);
185
+ const segments = expandMealDashSegments((0, segment_1.splitSigSegments)(input), options);
96
186
  const carry = {};
97
187
  const results = [];
98
188
  for (const segment of segments) {
@@ -132,7 +222,7 @@ function lintSig(input, options) {
132
222
  }
133
223
  function parseSigAsync(input, options) {
134
224
  return __awaiter(this, void 0, void 0, function* () {
135
- const segments = (0, segment_1.splitSigSegments)(input);
225
+ const segments = expandMealDashSegments((0, segment_1.splitSigSegments)(input), options);
136
226
  const carry = {};
137
227
  const results = [];
138
228
  for (const segment of segments) {
package/dist/schedule.js CHANGED
@@ -419,6 +419,69 @@ function expandWhenCodes(whenCodes, config, repeat) {
419
419
  return a.time.localeCompare(b.time);
420
420
  });
421
421
  }
422
+ const DEFAULT_WHEN_FALLBACK_CLOCKS = {
423
+ [types_1.EventTiming.Wake]: ["06:00:00"],
424
+ [types_1.EventTiming["Early Morning"]]: ["06:00:00"],
425
+ [types_1.EventTiming.Morning]: ["08:00:00"],
426
+ [types_1.EventTiming["Late Morning"]]: ["10:00:00"],
427
+ [types_1.EventTiming.Breakfast]: ["08:00:00"],
428
+ [types_1.EventTiming["Before Breakfast"]]: ["07:30:00"],
429
+ [types_1.EventTiming["After Breakfast"]]: ["08:30:00"],
430
+ [types_1.EventTiming.Noon]: ["12:00:00"],
431
+ [types_1.EventTiming.Lunch]: ["12:30:00"],
432
+ [types_1.EventTiming["Before Lunch"]]: ["12:00:00"],
433
+ [types_1.EventTiming["After Lunch"]]: ["13:00:00"],
434
+ [types_1.EventTiming["Early Afternoon"]]: ["14:00:00"],
435
+ [types_1.EventTiming.Afternoon]: ["15:00:00"],
436
+ [types_1.EventTiming["Late Afternoon"]]: ["16:00:00"],
437
+ [types_1.EventTiming["Early Evening"]]: ["18:00:00"],
438
+ [types_1.EventTiming.Evening]: ["19:00:00"],
439
+ [types_1.EventTiming["Late Evening"]]: ["20:00:00"],
440
+ [types_1.EventTiming.Dinner]: ["18:30:00"],
441
+ [types_1.EventTiming["Before Dinner"]]: ["18:00:00"],
442
+ [types_1.EventTiming["After Dinner"]]: ["19:00:00"],
443
+ [types_1.EventTiming.Night]: ["21:00:00"],
444
+ [types_1.EventTiming["Before Sleep"]]: ["22:00:00"],
445
+ [types_1.EventTiming["After Sleep"]]: ["06:30:00"],
446
+ [types_1.EventTiming.Meal]: ["08:00:00", "12:30:00", "18:30:00"],
447
+ [types_1.EventTiming["Before Meal"]]: ["07:30:00", "12:00:00", "18:00:00"],
448
+ [types_1.EventTiming["After Meal"]]: ["08:30:00", "13:00:00", "19:00:00"]
449
+ };
450
+ function inferWhenFallbackEntries(whenCodes, repeat) {
451
+ const entries = [];
452
+ const seen = new Set();
453
+ const addClock = (clock) => {
454
+ var _a;
455
+ const normalized = normalizeClock(clock);
456
+ const adjusted = repeat.offset
457
+ ? applyOffset(normalized, (_a = repeat.offset) !== null && _a !== void 0 ? _a : 0)
458
+ : { time: normalized, dayShift: 0 };
459
+ const key = `${adjusted.dayShift}|${adjusted.time}`;
460
+ if (seen.has(key)) {
461
+ return;
462
+ }
463
+ seen.add(key);
464
+ entries.push(adjusted);
465
+ };
466
+ for (const code of whenCodes) {
467
+ if (code === types_1.EventTiming.Immediate) {
468
+ continue;
469
+ }
470
+ const fallbackClocks = DEFAULT_WHEN_FALLBACK_CLOCKS[code];
471
+ if (!fallbackClocks) {
472
+ continue;
473
+ }
474
+ for (const clock of fallbackClocks) {
475
+ addClock(clock);
476
+ }
477
+ }
478
+ return entries.sort((a, b) => {
479
+ if (a.dayShift !== b.dayShift) {
480
+ return a.dayShift - b.dayShift;
481
+ }
482
+ return a.time.localeCompare(b.time);
483
+ });
484
+ }
422
485
  function mergeFrequencyDefaults(base, override) {
423
486
  var _a, _b, _c, _d;
424
487
  if (!base && !override) {
@@ -571,6 +634,11 @@ function nextDueDoses(dosage, options) {
571
634
  return a.time.localeCompare(b.time);
572
635
  });
573
636
  }
637
+ if (expanded.length === 0 &&
638
+ timeOfDayEntries.length === 0 &&
639
+ (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
640
+ expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
641
+ }
574
642
  const includesImmediate = (0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate);
575
643
  if (includesImmediate) {
576
644
  const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
@@ -731,6 +799,11 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
731
799
  return a.time.localeCompare(b.time);
732
800
  });
733
801
  }
802
+ if (expanded.length === 0 &&
803
+ timeOfDayEntries.length === 0 &&
804
+ (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
805
+ expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
806
+ }
734
807
  if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
735
808
  if (recordCandidate(orderedAt) && normalizedCount !== undefined && seen.size >= normalizedCount) {
736
809
  return count;
@@ -981,6 +1054,11 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
981
1054
  return a.time.localeCompare(b.time);
982
1055
  });
983
1056
  }
1057
+ if (expanded.length === 0 &&
1058
+ timeOfDayEntries.length === 0 &&
1059
+ (!repeat.frequency || !repeat.period || !repeat.periodUnit)) {
1060
+ expanded.push(...inferWhenFallbackEntries(whenCodes, repeat));
1061
+ }
984
1062
  if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
985
1063
  const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
986
1064
  if (!orderedAt || orderedAt >= from) {
package/dist/types.d.ts CHANGED
@@ -457,6 +457,13 @@ export interface ParseOptions extends FormatOptions {
457
457
  * `context.mealRelation` when provided.
458
458
  */
459
459
  smartMealExpansion?: boolean;
460
+ /**
461
+ * Enables parsing meal dash shorthand like `1-0-1` / `1-0-0-1` into
462
+ * multiple dosage clauses aligned to breakfast/lunch/dinner/(bedtime).
463
+ * Optional trailing `ac` / `pc` maps meal anchors to before/after meal
464
+ * variants.
465
+ */
466
+ enableMealDashSyntax?: boolean;
460
467
  /**
461
468
  * Controls which meal pair is assumed for twice-daily meal expansions.
462
469
  * Defaults to "breakfast+dinner" to mirror common clinical practice.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",