ezmedicationinput 0.1.20 → 0.1.22

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
@@ -293,8 +293,15 @@ You can specify the number of times (total count) the medication is supposed to
293
293
  - `context`: optional medication context (dosage form, strength, container
294
294
  metadata) used to infer default units when a sig omits explicit units. Pass
295
295
  `null` to explicitly disable context-based inference.
296
- - `smartMealExpansion`: when `true`, generic AC/PC/C tokens expand into specific EventTiming combinations (e.g. `1x2 po ac` → `ACM` + `ACV`).
296
+ - `smartMealExpansion`: when `true`, generic AC/PC/C meal abbreviations and
297
+ cadence-only instructions expand into concrete with-meal EventTiming
298
+ combinations (e.g. `1x3` → breakfast/lunch/dinner). This also respects
299
+ `context.mealRelation` when provided and only applies to schedules with four
300
+ or fewer daily doses.
297
301
  - `twoPerDayPair`: controls whether 2× AC/PC/C doses expand to breakfast+dinner (default) or breakfast+lunch.
302
+ - `assumeSingleDiscreteDose`: when `true`, missing discrete doses (such as
303
+ tablets or capsules) default to a single unit when the parser can infer a
304
+ countable unit from context.
298
305
  - `eventClock`: optional map of `EventTiming` codes to HH:mm strings that drives chronological ordering of parsed `when` values.
299
306
  - `allowHouseholdVolumeUnits`: defaults to `true`; set to `false` to ignore
300
307
  teaspoon/tablespoon units during parsing and suggestions.
package/dist/parser.js CHANGED
@@ -134,6 +134,36 @@ const SITE_FILLER_WORDS = new Set([
134
134
  "my"
135
135
  ]);
136
136
  const HOUSEHOLD_VOLUME_UNIT_SET = new Set(maps_1.HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase()));
137
+ const DISCRETE_UNIT_SET = new Set([
138
+ "tab",
139
+ "tabs",
140
+ "tablet",
141
+ "tablets",
142
+ "cap",
143
+ "caps",
144
+ "capsule",
145
+ "capsules",
146
+ "puff",
147
+ "puffs",
148
+ "spray",
149
+ "sprays",
150
+ "drop",
151
+ "drops",
152
+ "patch",
153
+ "patches",
154
+ "suppository",
155
+ "suppositories",
156
+ "implant",
157
+ "implants",
158
+ "piece",
159
+ "pieces",
160
+ "stick",
161
+ "sticks",
162
+ "pessary",
163
+ "pessaries",
164
+ "lozenge",
165
+ "lozenges"
166
+ ]);
137
167
  const OCULAR_DIRECTION_WORDS = new Set([
138
168
  "left",
139
169
  "right",
@@ -1067,13 +1097,12 @@ function reconcileMealTimingSpecificity(internal) {
1067
1097
  ]);
1068
1098
  }
1069
1099
  // Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
1070
- // EventTiming codes when the cadence makes the intent obvious.
1100
+ // EventTiming codes when the cadence or explicit meal abbreviations make the
1101
+ // intent obvious.
1071
1102
  function expandMealTimings(internal, options) {
1072
- var _a;
1073
- if (!(options === null || options === void 0 ? void 0 : options.smartMealExpansion)) {
1074
- return;
1075
- }
1076
- if (!internal.when.length) {
1103
+ var _a, _b, _c;
1104
+ const allowSmartExpansion = (options === null || options === void 0 ? void 0 : options.smartMealExpansion) === true;
1105
+ if (!allowSmartExpansion) {
1077
1106
  return;
1078
1107
  }
1079
1108
  if (internal.when.some((code) => SPECIFIC_MEAL_TIMINGS.has(code))) {
@@ -1083,6 +1112,14 @@ function expandMealTimings(internal, options) {
1083
1112
  if (!frequency || frequency < 1 || frequency > 4) {
1084
1113
  return;
1085
1114
  }
1115
+ const needsDefaultExpansion = internal.when.length === 0 && frequency >= 2;
1116
+ const hasBeforeMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["Before Meal"]);
1117
+ const hasAfterMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["After Meal"]);
1118
+ const hasWithMeal = (0, array_1.arrayIncludes)(internal.when, types_1.EventTiming.Meal);
1119
+ const hasGeneralMealToken = hasBeforeMeal || hasAfterMeal || hasWithMeal;
1120
+ if (!hasGeneralMealToken && !needsDefaultExpansion) {
1121
+ return;
1122
+ }
1086
1123
  if (internal.period !== undefined &&
1087
1124
  internal.periodUnit !== undefined &&
1088
1125
  (internal.periodUnit !== types_1.FhirPeriodUnit.Day || internal.period !== 1)) {
@@ -1099,28 +1136,36 @@ function expandMealTimings(internal, options) {
1099
1136
  if (internal.frequencyMax !== undefined || internal.periodMax !== undefined) {
1100
1137
  return;
1101
1138
  }
1102
- const pairPreference = (_a = options.twoPerDayPair) !== null && _a !== void 0 ? _a : "breakfast+dinner";
1139
+ const pairPreference = (_a = options === null || options === void 0 ? void 0 : options.twoPerDayPair) !== null && _a !== void 0 ? _a : "breakfast+dinner";
1103
1140
  const replacements = [];
1104
- if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["Before Meal"])) {
1105
- const specifics = computeMealExpansions("before", frequency, pairPreference);
1141
+ const addReplacement = (general, base, removeGeneral) => {
1142
+ const specifics = computeMealExpansions(base, frequency, pairPreference);
1106
1143
  if (specifics) {
1107
- replacements.push({ general: types_1.EventTiming["Before Meal"], specifics });
1144
+ replacements.push({ general, specifics, removeGeneral });
1108
1145
  }
1146
+ };
1147
+ if (hasBeforeMeal) {
1148
+ addReplacement(types_1.EventTiming["Before Meal"], "before", true);
1109
1149
  }
1110
- if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["After Meal"])) {
1111
- const specifics = computeMealExpansions("after", frequency, pairPreference);
1112
- if (specifics) {
1113
- replacements.push({ general: types_1.EventTiming["After Meal"], specifics });
1114
- }
1150
+ if (hasAfterMeal) {
1151
+ addReplacement(types_1.EventTiming["After Meal"], "after", true);
1115
1152
  }
1116
- if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming.Meal)) {
1117
- const specifics = computeMealExpansions("with", frequency, pairPreference);
1118
- if (specifics) {
1119
- replacements.push({ general: types_1.EventTiming.Meal, specifics });
1120
- }
1153
+ if (hasWithMeal) {
1154
+ addReplacement(types_1.EventTiming.Meal, "with", true);
1155
+ }
1156
+ if (needsDefaultExpansion) {
1157
+ const relation = (_c = (_b = options === null || options === void 0 ? void 0 : options.context) === null || _b === void 0 ? void 0 : _b.mealRelation) !== null && _c !== void 0 ? _c : types_1.EventTiming.Meal;
1158
+ const base = relation === types_1.EventTiming["Before Meal"]
1159
+ ? "before"
1160
+ : relation === types_1.EventTiming["After Meal"]
1161
+ ? "after"
1162
+ : "with";
1163
+ addReplacement(relation, base, false);
1121
1164
  }
1122
- for (const { general, specifics } of replacements) {
1123
- removeWhen(internal.when, general);
1165
+ for (const { general, specifics, removeGeneral } of replacements) {
1166
+ if (removeGeneral) {
1167
+ removeWhen(internal.when, general);
1168
+ }
1124
1169
  for (const specific of specifics) {
1125
1170
  addWhen(internal.when, specific);
1126
1171
  }
@@ -1857,6 +1902,13 @@ function parseInternal(input, options) {
1857
1902
  internal.unit = fallbackUnit;
1858
1903
  }
1859
1904
  }
1905
+ if ((options === null || options === void 0 ? void 0 : options.assumeSingleDiscreteDose) &&
1906
+ internal.dose === undefined &&
1907
+ internal.doseRange === undefined &&
1908
+ internal.unit !== undefined &&
1909
+ isDiscreteUnit(internal.unit)) {
1910
+ internal.dose = 1;
1911
+ }
1860
1912
  // Frequency defaults when timing code implies it
1861
1913
  if (internal.frequency === undefined &&
1862
1914
  internal.period === undefined &&
@@ -3264,6 +3316,12 @@ function enforceHouseholdUnitPolicy(unit, options) {
3264
3316
  }
3265
3317
  return unit;
3266
3318
  }
3319
+ function isDiscreteUnit(unit) {
3320
+ if (!unit) {
3321
+ return false;
3322
+ }
3323
+ return DISCRETE_UNIT_SET.has(unit.trim().toLowerCase());
3324
+ }
3267
3325
  function inferUnitFromRouteHints(internal) {
3268
3326
  if (internal.routeCode) {
3269
3327
  const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[internal.routeCode];
package/dist/types.d.ts CHANGED
@@ -285,6 +285,7 @@ export interface MedicationContext {
285
285
  containerValue?: number;
286
286
  containerUnit?: string;
287
287
  defaultUnit?: string;
288
+ mealRelation?: (typeof EventTiming)["Before Meal"] | (typeof EventTiming)["After Meal"] | (typeof EventTiming)["Meal"];
288
289
  }
289
290
  export interface FormatOptions {
290
291
  locale?: "en" | "th" | string;
@@ -428,9 +429,17 @@ export interface ParseOptions extends FormatOptions {
428
429
  eventClock?: EventClockMap;
429
430
  allowDiscouraged?: boolean;
430
431
  /**
431
- * When enabled the parser will expand generic meal timing tokens (AC/PC/C)
432
- * into specific breakfast/lunch/dinner (and bedtime) EventTiming entries
433
- * based on the detected daily frequency.
432
+ * When enabled the parser will assume a single discrete unit (e.g., one
433
+ * tablet/capsule) when no explicit dose is provided and the inferred unit is
434
+ * countable.
435
+ */
436
+ assumeSingleDiscreteDose?: boolean;
437
+ /**
438
+ * Enables inferring with-meal timings when explicit meal language is present
439
+ * or implied by cadence alone. Generic meal abbreviations (AC/PC/C) and
440
+ * cadence-only instructions expand into specific breakfast/lunch/dinner (and
441
+ * bedtime) EventTiming entries. Works in conjunction with
442
+ * `context.mealRelation` when provided.
434
443
  */
435
444
  smartMealExpansion?: boolean;
436
445
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",