ezmedicationinput 0.1.41 → 0.1.43

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
@@ -139,6 +139,8 @@ Use either helper depending on your source:
139
139
 
140
140
  - `formatParseBatch(batch, style?, separator?)` when you already have `parseSig` output.
141
141
  - `formatSigBatch(dosages, style?, { separator })` when you have an array of FHIR `Dosage` entries.
142
+ - `formatSig(dosage, style?, options?)` / `fromFhirDosage(dosage, options?)` when you want
143
+ locale-aware long-text rendering controls for a single dosage.
142
144
 
143
145
  ```ts
144
146
  import { formatParseBatch, formatSigBatch, parseSig } from "ezmedicationinput";
@@ -152,6 +154,31 @@ const shortFromFhir = formatSigBatch(batch.items.map((item) => item.fhir), "shor
152
154
  // => same combined short sig text
153
155
  ```
154
156
 
157
+ Formatting options:
158
+
159
+ - `locale`: selects the registered localization, such as `"en"` or `"th"`.
160
+ - `i18n`: overrides or augments the registered localization callbacks.
161
+ - `groupMealTimingsByRelation`: compacts repeated meal relation phrases when all
162
+ meal anchors share the same relation.
163
+ Example EN: `after breakfast, lunch and dinner`
164
+ Example TH: `หลังอาหารเช้า กลางวัน และเย็น`
165
+ - `includeTimesPerDaySummary`: prepends a daily count when the formatter can
166
+ safely infer one from explicit daily anchors and no cadence already exists.
167
+ Example EN: `three times daily after breakfast, lunch and dinner`
168
+ Example TH: `วันละ 3 ครั้ง หลังอาหารเช้า กลางวัน และเย็น`
169
+
170
+ Notes:
171
+
172
+ - `groupMealTimingsByRelation` only applies to homogeneous specific meal anchors
173
+ (`before breakfast/lunch/dinner`, `after breakfast/lunch/dinner`, or `with breakfast/lunch/dinner`).
174
+ When additional non-meal daily anchors exist, the formatter groups the meal
175
+ subset and leaves the extra anchors explicit.
176
+ Example EN: `before breakfast, lunch and dinner and at bedtime`
177
+ Example TH: `ก่อนอาหารเช้า กลางวัน และเย็น และก่อนนอน`
178
+ - `includeTimesPerDaySummary` is independent from meal grouping. It counts
179
+ explicit daily anchors only when no `frequency`, `timingCode`, interval, or
180
+ day-of-week cadence is already present.
181
+
155
182
  ### Sig (directions) suggestions
156
183
 
157
184
  Use `suggestSig` to drive autocomplete experiences while the clinician is
package/dist/context.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- import { MedicationContext } from "./types";
1
+ import { MedicationContext, RouteCode } from "./types";
2
2
  export declare function normalizeDosageForm(form: string | undefined): string | undefined;
3
3
  export declare function inferUnitFromContext(ctx: MedicationContext | undefined): string | undefined;
4
+ export declare function inferRouteFromContext(ctx: MedicationContext | undefined): RouteCode | undefined;
package/dist/context.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeDosageForm = normalizeDosageForm;
4
4
  exports.inferUnitFromContext = inferUnitFromContext;
5
+ exports.inferRouteFromContext = inferRouteFromContext;
5
6
  const maps_1 = require("./maps");
6
7
  function normalizeDosageForm(form) {
7
8
  var _a;
@@ -32,3 +33,18 @@ function inferUnitFromContext(ctx) {
32
33
  }
33
34
  return undefined;
34
35
  }
36
+ function inferRouteFromContext(ctx) {
37
+ var _a;
38
+ if (!(ctx === null || ctx === void 0 ? void 0 : ctx.dosageForm)) {
39
+ return undefined;
40
+ }
41
+ const normalized = normalizeDosageForm(ctx.dosageForm);
42
+ if (!normalized) {
43
+ return undefined;
44
+ }
45
+ const snomed = maps_1.KNOWN_TMT_DOSAGE_FORM_TO_SNOMED_ROUTE[normalized];
46
+ if (!snomed) {
47
+ return (_a = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized]) === null || _a === void 0 ? void 0 : _a.code;
48
+ }
49
+ return maps_1.ROUTE_BY_SNOMED[snomed];
50
+ }
package/dist/format.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  import { ParsedSigInternal } from "./internal-types";
2
2
  import type { SigLocalization } from "./i18n";
3
- export declare function formatInternal(internal: ParsedSigInternal, style: "short" | "long", localization?: SigLocalization): string;
3
+ import { type TimingSummaryOptions } from "./timing-summary";
4
+ export declare function formatInternal(internal: ParsedSigInternal, style: "short" | "long", localization?: SigLocalization, options?: TimingSummaryOptions): string;
package/dist/format.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatInternal = formatInternal;
4
4
  const types_1 = require("./types");
5
+ const timing_summary_1 = require("./timing-summary");
5
6
  const ROUTE_SHORT = {
6
7
  [types_1.RouteCode["Oral route"]]: "PO",
7
8
  [types_1.RouteCode["Sublingual route"]]: "SL",
@@ -56,6 +57,12 @@ const DAY_NAMES = {
56
57
  sat: "Saturday",
57
58
  sun: "Sunday"
58
59
  };
60
+ const EN_TIMES_PER_DAY = {
61
+ 1: "once daily",
62
+ 2: "twice daily",
63
+ 3: "three times daily",
64
+ 4: "four times daily"
65
+ };
59
66
  const DEFAULT_ROUTE_GRAMMAR = { verb: "Use" };
60
67
  const ROUTE_GRAMMAR = {
61
68
  [types_1.RouteCode["Oral route"]]: { verb: "Take", routePhrase: "by mouth" },
@@ -201,14 +208,10 @@ function describeFrequency(internal) {
201
208
  return `${stripTrailingZero(frequency)} to ${stripTrailingZero(frequencyMax)} times daily`;
202
209
  }
203
210
  if (frequency && periodUnit === types_1.FhirPeriodUnit.Day && (!period || period === 1)) {
204
- if (frequency === 1)
205
- return "once daily";
206
- if (frequency === 2)
207
- return "twice daily";
208
- if (frequency === 3)
209
- return "three times daily";
210
- if (frequency === 4)
211
- return "four times daily";
211
+ const dailyText = EN_TIMES_PER_DAY[frequency];
212
+ if (dailyText) {
213
+ return dailyText;
214
+ }
212
215
  return `${stripTrailingZero(frequency)} times daily`;
213
216
  }
214
217
  if (periodUnit === types_1.FhirPeriodUnit.Hour && period) {
@@ -271,6 +274,16 @@ function describeFrequency(internal) {
271
274
  }
272
275
  return undefined;
273
276
  }
277
+ function describeFrequencyCount(count) {
278
+ if (!count || count <= 0) {
279
+ return undefined;
280
+ }
281
+ const dailyText = EN_TIMES_PER_DAY[count];
282
+ if (dailyText) {
283
+ return dailyText;
284
+ }
285
+ return `${stripTrailingZero(count)} times daily`;
286
+ }
274
287
  function formatDoseShort(internal) {
275
288
  if (internal.doseRange) {
276
289
  const { low, high } = internal.doseRange;
@@ -304,42 +317,90 @@ function formatDoseLong(internal) {
304
317
  }
305
318
  return undefined;
306
319
  }
307
- function collectWhenPhrases(internal) {
320
+ function summarizeMealTimingGroup(group) {
321
+ let relationText = "with";
322
+ if (group.relation === "before") {
323
+ relationText = "before";
324
+ }
325
+ else if (group.relation === "after") {
326
+ relationText = "after";
327
+ }
328
+ return `${relationText} ${joinWithAnd(group.meals)}`;
329
+ }
330
+ function collectWhenPhrases(internal, options) {
331
+ var _a, _b;
308
332
  if (!internal.when.length) {
309
333
  return [];
310
334
  }
311
335
  const unique = [];
312
336
  const seen = new Set();
337
+ let hasSpecificAfter = false;
338
+ let hasSpecificBefore = false;
339
+ let hasSpecificWith = false;
313
340
  for (const code of internal.when) {
314
341
  if (!seen.has(code)) {
315
342
  seen.add(code);
316
343
  unique.push(code);
317
- }
318
- }
319
- const hasSpecificAfter = unique.some((code) => code === types_1.EventTiming["After Breakfast"] ||
320
- code === types_1.EventTiming["After Lunch"] ||
321
- code === types_1.EventTiming["After Dinner"]);
322
- const hasSpecificBefore = unique.some((code) => code === types_1.EventTiming["Before Breakfast"] ||
323
- code === types_1.EventTiming["Before Lunch"] ||
324
- code === types_1.EventTiming["Before Dinner"]);
325
- const hasSpecificWith = unique.some((code) => code === types_1.EventTiming.Breakfast ||
326
- code === types_1.EventTiming.Lunch ||
327
- code === types_1.EventTiming.Dinner);
328
- return unique
329
- .filter((code) => {
344
+ if (code === types_1.EventTiming["After Breakfast"] ||
345
+ code === types_1.EventTiming["After Lunch"] ||
346
+ code === types_1.EventTiming["After Dinner"]) {
347
+ hasSpecificAfter = true;
348
+ }
349
+ if (code === types_1.EventTiming["Before Breakfast"] ||
350
+ code === types_1.EventTiming["Before Lunch"] ||
351
+ code === types_1.EventTiming["Before Dinner"]) {
352
+ hasSpecificBefore = true;
353
+ }
354
+ if (code === types_1.EventTiming.Breakfast ||
355
+ code === types_1.EventTiming.Lunch ||
356
+ code === types_1.EventTiming.Dinner) {
357
+ hasSpecificWith = true;
358
+ }
359
+ }
360
+ }
361
+ const filtered = [];
362
+ for (let i = 0; i < unique.length; i += 1) {
363
+ const code = unique[i];
330
364
  if (code === types_1.EventTiming["After Meal"] && hasSpecificAfter) {
331
- return false;
365
+ continue;
332
366
  }
333
367
  if (code === types_1.EventTiming["Before Meal"] && hasSpecificBefore) {
334
- return false;
368
+ continue;
335
369
  }
336
370
  if (code === types_1.EventTiming.Meal && hasSpecificWith) {
337
- return false;
338
- }
339
- return true;
340
- })
341
- .map((code) => { var _a; return (_a = WHEN_TEXT[code]) !== null && _a !== void 0 ? _a : code; })
342
- .filter((text) => Boolean(text));
371
+ continue;
372
+ }
373
+ filtered.push(code);
374
+ }
375
+ const mealGroup = (0, timing_summary_1.getMealTimingGroup)(filtered, options);
376
+ if (mealGroup) {
377
+ const groupedCodes = new Set(mealGroup.codes);
378
+ const phrases = [];
379
+ let insertedGroup = false;
380
+ for (let i = 0; i < filtered.length; i += 1) {
381
+ const code = filtered[i];
382
+ if (groupedCodes.has(code)) {
383
+ if (!insertedGroup) {
384
+ phrases.push(summarizeMealTimingGroup(mealGroup));
385
+ insertedGroup = true;
386
+ }
387
+ continue;
388
+ }
389
+ const text = (_a = WHEN_TEXT[code]) !== null && _a !== void 0 ? _a : code;
390
+ if (text) {
391
+ phrases.push(text);
392
+ }
393
+ }
394
+ return phrases;
395
+ }
396
+ const phrases = [];
397
+ for (let i = 0; i < filtered.length; i += 1) {
398
+ const text = (_b = WHEN_TEXT[filtered[i]]) !== null && _b !== void 0 ? _b : filtered[i];
399
+ if (text) {
400
+ phrases.push(text);
401
+ }
402
+ }
403
+ return phrases;
343
404
  }
344
405
  function joinWithAnd(parts) {
345
406
  if (!parts.length) {
@@ -485,10 +546,10 @@ function describeDayOfWeek(internal) {
485
546
  }
486
547
  return `on ${joinWithAnd(days)}`;
487
548
  }
488
- function formatInternal(internal, style, localization) {
549
+ function formatInternal(internal, style, localization, options) {
489
550
  const defaults = {
490
551
  short: formatShort(internal),
491
- long: formatLong(internal)
552
+ long: formatLong(internal, options)
492
553
  };
493
554
  if (!localization) {
494
555
  return defaults[style];
@@ -499,6 +560,8 @@ function formatInternal(internal, style, localization) {
499
560
  style: "short",
500
561
  internal,
501
562
  defaultText: defaults.short,
563
+ groupMealTimingsByRelation: Boolean(options === null || options === void 0 ? void 0 : options.groupMealTimingsByRelation),
564
+ includeTimesPerDaySummary: Boolean(options === null || options === void 0 ? void 0 : options.includeTimesPerDaySummary),
502
565
  formatDefault
503
566
  };
504
567
  return localization.formatShort(context);
@@ -508,6 +571,8 @@ function formatInternal(internal, style, localization) {
508
571
  style: "long",
509
572
  internal,
510
573
  defaultText: defaults.long,
574
+ groupMealTimingsByRelation: Boolean(options === null || options === void 0 ? void 0 : options.groupMealTimingsByRelation),
575
+ includeTimesPerDaySummary: Boolean(options === null || options === void 0 ? void 0 : options.includeTimesPerDaySummary),
511
576
  formatDefault
512
577
  };
513
578
  return localization.formatLong(context);
@@ -578,15 +643,15 @@ function formatShort(internal) {
578
643
  }
579
644
  return parts.filter(Boolean).join(" ");
580
645
  }
581
- function formatLong(internal) {
582
- var _a, _b;
646
+ function formatLong(internal, options) {
647
+ var _a, _b, _c;
583
648
  const grammar = resolveRouteGrammar(internal);
584
649
  const dosePart = (_a = formatDoseLong(internal)) !== null && _a !== void 0 ? _a : "the medication";
585
650
  const sitePart = formatSite(internal, grammar);
586
651
  const routePart = buildRoutePhrase(internal, grammar, Boolean(sitePart));
587
- const frequencyPart = describeFrequency(internal);
588
- const eventParts = collectWhenPhrases(internal);
589
- if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
652
+ const frequencyPart = (_b = describeFrequency(internal)) !== null && _b !== void 0 ? _b : describeFrequencyCount((0, timing_summary_1.inferDailyOccurrenceCount)(internal, options));
653
+ const eventParts = collectWhenPhrases(internal, options);
654
+ if ((_c = internal.timeOfDay) === null || _c === void 0 ? void 0 : _c.length) {
590
655
  const timeStrings = [];
591
656
  for (const time of internal.timeOfDay) {
592
657
  const parts = time.split(":");
package/dist/i18n.d.ts CHANGED
@@ -3,6 +3,8 @@ export interface SigFormatContext {
3
3
  readonly style: "short" | "long";
4
4
  readonly internal: ParsedSigInternal;
5
5
  readonly defaultText: string;
6
+ readonly groupMealTimingsByRelation: boolean;
7
+ readonly includeTimesPerDaySummary: boolean;
6
8
  formatDefault(style: "short" | "long"): string;
7
9
  }
8
10
  export interface SigShortContext extends SigFormatContext {
package/dist/i18n.js CHANGED
@@ -6,6 +6,7 @@ exports.getRegisteredSigLocalizations = getRegisteredSigLocalizations;
6
6
  exports.resolveSigLocalization = resolveSigLocalization;
7
7
  exports.resolveSigTranslation = resolveSigTranslation;
8
8
  const types_1 = require("./types");
9
+ const timing_summary_1 = require("./timing-summary");
9
10
  const REGISTERED_LOCALIZATIONS = new Map();
10
11
  function registerSigLocalization(localization) {
11
12
  REGISTERED_LOCALIZATIONS.set(localization.locale.toLowerCase(), localization);
@@ -65,7 +66,10 @@ function createThaiLocalization() {
65
66
  return {
66
67
  locale: "th",
67
68
  formatShort: ({ internal }) => formatShortThai(internal),
68
- formatLong: ({ internal }) => formatLongThai(internal)
69
+ formatLong: ({ internal, groupMealTimingsByRelation, includeTimesPerDaySummary }) => formatLongThai(internal, {
70
+ groupMealTimingsByRelation,
71
+ includeTimesPerDaySummary
72
+ })
69
73
  };
70
74
  }
71
75
  registerSigLocalization(createThaiLocalization());
@@ -125,6 +129,12 @@ const DAY_NAMES_THAI = {
125
129
  sat: "วันเสาร์",
126
130
  sun: "วันอาทิตย์"
127
131
  };
132
+ const TH_TIMES_PER_DAY = {
133
+ 1: "วันละครั้ง",
134
+ 2: "วันละ 2 ครั้ง",
135
+ 3: "วันละ 3 ครั้ง",
136
+ 4: "วันละ 4 ครั้ง"
137
+ };
128
138
  exports.THAI_SITE_TRANSLATIONS = {
129
139
  eye: "ตา",
130
140
  eyes: "ตา",
@@ -231,7 +241,6 @@ const THAI_ROUTE_GRAMMAR = {
231
241
  [types_1.RouteCode["Buccal route"]]: { verb: "อมกระพุ้งแก้ม", routePhrase: "ที่กระพุ้งแก้ม" },
232
242
  [types_1.RouteCode["Respiratory tract route (qualifier value)"]]: {
233
243
  verb: "สูด",
234
- routePhrase: ({ hasSite }) => (hasSite ? undefined : "โดยการสูดดม"),
235
244
  sitePreposition: "ที่"
236
245
  },
237
246
  [types_1.RouteCode["Nasal route"]]: {
@@ -291,12 +300,18 @@ const THAI_ROUTE_GRAMMAR = {
291
300
  }
292
301
  };
293
302
  function resolveRouteGrammarThai(internal) {
294
- var _a;
303
+ var _a, _b, _c;
295
304
  if (internal.routeCode && THAI_ROUTE_GRAMMAR[internal.routeCode]) {
296
305
  return (_a = THAI_ROUTE_GRAMMAR[internal.routeCode]) !== null && _a !== void 0 ? _a : DEFAULT_THAI_ROUTE_GRAMMAR;
297
306
  }
298
307
  const grammar = grammarFromRouteTextThai(internal.routeText);
299
- return grammar !== null && grammar !== void 0 ? grammar : DEFAULT_THAI_ROUTE_GRAMMAR;
308
+ if (grammar) {
309
+ return grammar;
310
+ }
311
+ if (((_b = internal.unit) === null || _b === void 0 ? void 0 : _b.trim().toLowerCase()) === "puff") {
312
+ return (_c = THAI_ROUTE_GRAMMAR[types_1.RouteCode["Respiratory tract route (qualifier value)"]]) !== null && _c !== void 0 ? _c : DEFAULT_THAI_ROUTE_GRAMMAR;
313
+ }
314
+ return DEFAULT_THAI_ROUTE_GRAMMAR;
300
315
  }
301
316
  function grammarFromRouteTextThai(text) {
302
317
  if (!text)
@@ -418,14 +433,10 @@ function describeFrequencyThai(internal) {
418
433
  return `วันละ ${stripTrailingZero(frequency)} ถึง ${stripTrailingZero(frequencyMax)} ครั้ง`;
419
434
  }
420
435
  if (frequency && periodUnit === types_1.FhirPeriodUnit.Day && (!period || period === 1)) {
421
- if (frequency === 1)
422
- return "วันละครั้ง";
423
- if (frequency === 2)
424
- return "วันละ 2 ครั้ง";
425
- if (frequency === 3)
426
- return "วันละ 3 ครั้ง";
427
- if (frequency === 4)
428
- return "วันละ 4 ครั้ง";
436
+ const dailyText = TH_TIMES_PER_DAY[frequency];
437
+ if (dailyText) {
438
+ return dailyText;
439
+ }
429
440
  return `วันละ ${stripTrailingZero(frequency)} ครั้ง`;
430
441
  }
431
442
  if (periodUnit === types_1.FhirPeriodUnit.Hour && period) {
@@ -485,42 +496,122 @@ function describeFrequencyThai(internal) {
485
496
  }
486
497
  return undefined;
487
498
  }
488
- function collectWhenPhrasesThai(internal) {
499
+ function describeFrequencyCountThai(count) {
500
+ if (!count || count <= 0) {
501
+ return undefined;
502
+ }
503
+ const dailyText = TH_TIMES_PER_DAY[count];
504
+ if (dailyText) {
505
+ return dailyText;
506
+ }
507
+ return `วันละ ${stripTrailingZero(count)} ครั้ง`;
508
+ }
509
+ function joinMealNamesThai(parts) {
510
+ if (parts.length === 0) {
511
+ return "";
512
+ }
513
+ if (parts.length === 1) {
514
+ return parts[0];
515
+ }
516
+ if (parts.length === 2) {
517
+ return `${parts[0]} และ${parts[1]}`;
518
+ }
519
+ let text = parts[0];
520
+ for (let i = 1; i < parts.length - 1; i += 1) {
521
+ text += ` ${parts[i]}`;
522
+ }
523
+ return `${text} และ${parts[parts.length - 1]}`;
524
+ }
525
+ function summarizeMealTimingGroupThai(group) {
526
+ const relationText = {
527
+ before: "ก่อนอาหาร",
528
+ after: "หลังอาหาร",
529
+ with: "พร้อมอาหาร"
530
+ };
531
+ const mealText = {
532
+ breakfast: "เช้า",
533
+ lunch: "กลางวัน",
534
+ dinner: "เย็น"
535
+ };
536
+ const meals = [];
537
+ for (let i = 0; i < group.meals.length; i += 1) {
538
+ meals.push(mealText[group.meals[i]]);
539
+ }
540
+ return `${relationText[group.relation]}${joinMealNamesThai(meals)}`;
541
+ }
542
+ function collectWhenPhrasesThai(internal, options) {
489
543
  if (!internal.when.length) {
490
544
  return [];
491
545
  }
492
546
  const unique = [];
493
547
  const seen = new Set();
548
+ let hasSpecificAfter = false;
549
+ let hasSpecificBefore = false;
550
+ let hasSpecificWith = false;
494
551
  for (const code of internal.when) {
495
552
  if (!seen.has(code)) {
496
553
  seen.add(code);
497
554
  unique.push(code);
555
+ if (code === types_1.EventTiming["After Breakfast"] ||
556
+ code === types_1.EventTiming["After Lunch"] ||
557
+ code === types_1.EventTiming["After Dinner"]) {
558
+ hasSpecificAfter = true;
559
+ }
560
+ if (code === types_1.EventTiming["Before Breakfast"] ||
561
+ code === types_1.EventTiming["Before Lunch"] ||
562
+ code === types_1.EventTiming["Before Dinner"]) {
563
+ hasSpecificBefore = true;
564
+ }
565
+ if (code === types_1.EventTiming.Breakfast ||
566
+ code === types_1.EventTiming.Lunch ||
567
+ code === types_1.EventTiming.Dinner) {
568
+ hasSpecificWith = true;
569
+ }
498
570
  }
499
571
  }
500
- const hasSpecificAfter = unique.some((code) => code === types_1.EventTiming["After Breakfast"] ||
501
- code === types_1.EventTiming["After Lunch"] ||
502
- code === types_1.EventTiming["After Dinner"]);
503
- const hasSpecificBefore = unique.some((code) => code === types_1.EventTiming["Before Breakfast"] ||
504
- code === types_1.EventTiming["Before Lunch"] ||
505
- code === types_1.EventTiming["Before Dinner"]);
506
- const hasSpecificWith = unique.some((code) => code === types_1.EventTiming.Breakfast ||
507
- code === types_1.EventTiming.Lunch ||
508
- code === types_1.EventTiming.Dinner);
509
- return unique
510
- .filter((code) => {
572
+ const filtered = [];
573
+ for (let i = 0; i < unique.length; i += 1) {
574
+ const code = unique[i];
511
575
  if (code === types_1.EventTiming["After Meal"] && hasSpecificAfter) {
512
- return false;
576
+ continue;
513
577
  }
514
578
  if (code === types_1.EventTiming["Before Meal"] && hasSpecificBefore) {
515
- return false;
579
+ continue;
516
580
  }
517
581
  if (code === types_1.EventTiming.Meal && hasSpecificWith) {
518
- return false;
582
+ continue;
519
583
  }
520
- return true;
521
- })
522
- .map((code) => { var _a; return (_a = WHEN_TEXT_THAI[code]) !== null && _a !== void 0 ? _a : undefined; })
523
- .filter((text) => Boolean(text));
584
+ filtered.push(code);
585
+ }
586
+ const mealGroup = (0, timing_summary_1.getMealTimingGroup)(filtered, options);
587
+ if (mealGroup) {
588
+ const groupedCodes = new Set(mealGroup.codes);
589
+ const phrases = [];
590
+ let insertedGroup = false;
591
+ for (let i = 0; i < filtered.length; i += 1) {
592
+ const code = filtered[i];
593
+ if (groupedCodes.has(code)) {
594
+ if (!insertedGroup) {
595
+ phrases.push(summarizeMealTimingGroupThai(mealGroup));
596
+ insertedGroup = true;
597
+ }
598
+ continue;
599
+ }
600
+ const text = WHEN_TEXT_THAI[code];
601
+ if (text) {
602
+ phrases.push(text);
603
+ }
604
+ }
605
+ return phrases;
606
+ }
607
+ const phrases = [];
608
+ for (let i = 0; i < filtered.length; i += 1) {
609
+ const text = WHEN_TEXT_THAI[filtered[i]];
610
+ if (text) {
611
+ phrases.push(text);
612
+ }
613
+ }
614
+ return phrases;
524
615
  }
525
616
  function joinWithAndThai(parts) {
526
617
  if (!parts.length) {
@@ -606,7 +697,7 @@ function buildRoutePhraseThai(internal, grammar, hasSite) {
606
697
  return "ทางจมูก";
607
698
  }
608
699
  if (normalized.includes("inhal")) {
609
- return "โดยการสูดดม";
700
+ return undefined;
610
701
  }
611
702
  return text;
612
703
  }
@@ -710,15 +801,15 @@ function formatShortThai(internal) {
710
801
  }
711
802
  return parts.filter(Boolean).join(" ");
712
803
  }
713
- function formatLongThai(internal) {
714
- var _a, _b;
804
+ function formatLongThai(internal, options) {
805
+ var _a, _b, _c;
715
806
  const grammar = resolveRouteGrammarThai(internal);
716
807
  const dosePart = (_a = formatDoseThaiLong(internal)) !== null && _a !== void 0 ? _a : "ยา";
717
808
  const sitePart = formatSiteThai(internal, grammar);
718
809
  const routePart = buildRoutePhraseThai(internal, grammar, Boolean(sitePart));
719
- const frequencyPart = describeFrequencyThai(internal);
720
- const eventParts = collectWhenPhrasesThai(internal);
721
- if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
810
+ const frequencyPart = (_b = describeFrequencyThai(internal)) !== null && _b !== void 0 ? _b : describeFrequencyCountThai((0, timing_summary_1.inferDailyOccurrenceCount)(internal, options));
811
+ const eventParts = collectWhenPhrasesThai(internal, options);
812
+ if ((_c = internal.timeOfDay) === null || _c === void 0 ? void 0 : _c.length) {
722
813
  const timeStrings = [];
723
814
  for (const time of internal.timeOfDay) {
724
815
  const parts = time.split(":");
package/dist/index.js CHANGED
@@ -443,7 +443,7 @@ function parseSigAsync(input, options) {
443
443
  function formatSig(dosage, style = "short", options) {
444
444
  const internal = (0, fhir_1.internalFromFhir)(dosage);
445
445
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
446
- return (0, format_1.formatInternal)(internal, style, localization);
446
+ return (0, format_1.formatInternal)(internal, style, localization, options);
447
447
  }
448
448
  function formatSigBatch(dosages, style = "short", options) {
449
449
  var _a;
@@ -464,12 +464,13 @@ function formatParseBatch(batch, style = "short", separator = ", ") {
464
464
  return texts.join(separator);
465
465
  }
466
466
  function fromFhirDosage(dosage, options) {
467
- var _a, _b, _c, _d, _e, _f;
467
+ var _a, _b, _c, _d, _e;
468
468
  const internal = (0, fhir_1.internalFromFhir)(dosage);
469
469
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
470
- const shortText = (0, format_1.formatInternal)(internal, "short", localization);
471
- const computedLong = (0, format_1.formatInternal)(internal, "long", localization);
472
- const longText = localization ? computedLong : (_a = dosage.text) !== null && _a !== void 0 ? _a : computedLong;
470
+ const shortText = (0, format_1.formatInternal)(internal, "short", localization, options);
471
+ const computedLong = (0, format_1.formatInternal)(internal, "long", localization, options);
472
+ const longText = computedLong || dosage.text || "";
473
+ dosage.text = longText;
473
474
  return {
474
475
  fhir: dosage,
475
476
  shortText,
@@ -480,10 +481,10 @@ function fromFhirDosage(dosage, options) {
480
481
  normalized: {
481
482
  route: internal.routeCode,
482
483
  unit: internal.unit,
483
- site: internal.siteText || ((_b = internal.siteCoding) === null || _b === void 0 ? void 0 : _b.code)
484
+ site: internal.siteText || ((_a = internal.siteCoding) === null || _a === void 0 ? void 0 : _a.code)
484
485
  ? {
485
486
  text: internal.siteText,
486
- coding: ((_c = internal.siteCoding) === null || _c === void 0 ? void 0 : _c.code)
487
+ coding: ((_b = internal.siteCoding) === null || _b === void 0 ? void 0 : _b.code)
487
488
  ? {
488
489
  code: internal.siteCoding.code,
489
490
  display: internal.siteCoding.display,
@@ -492,10 +493,10 @@ function fromFhirDosage(dosage, options) {
492
493
  : undefined
493
494
  }
494
495
  : undefined,
495
- prnReason: internal.asNeededReason || ((_d = internal.asNeededReasonCoding) === null || _d === void 0 ? void 0 : _d.code)
496
+ prnReason: internal.asNeededReason || ((_c = internal.asNeededReasonCoding) === null || _c === void 0 ? void 0 : _c.code)
496
497
  ? {
497
498
  text: internal.asNeededReason,
498
- coding: ((_e = internal.asNeededReasonCoding) === null || _e === void 0 ? void 0 : _e.code)
499
+ coding: ((_d = internal.asNeededReasonCoding) === null || _d === void 0 ? void 0 : _d.code)
499
500
  ? {
500
501
  code: internal.asNeededReasonCoding.code,
501
502
  display: internal.asNeededReasonCoding.display,
@@ -504,7 +505,7 @@ function fromFhirDosage(dosage, options) {
504
505
  : undefined
505
506
  }
506
507
  : undefined,
507
- additionalInstructions: ((_f = internal.additionalInstructions) === null || _f === void 0 ? void 0 : _f.length)
508
+ additionalInstructions: ((_e = internal.additionalInstructions) === null || _e === void 0 ? void 0 : _e.length)
508
509
  ? internal.additionalInstructions.map((instruction) => {
509
510
  var _a;
510
511
  return ({
@@ -526,8 +527,8 @@ function fromFhirDosage(dosage, options) {
526
527
  function buildParseResult(internal, options) {
527
528
  var _a, _b, _c;
528
529
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
529
- const shortText = (0, format_1.formatInternal)(internal, "short", localization);
530
- const longText = (0, format_1.formatInternal)(internal, "long", localization);
530
+ const shortText = (0, format_1.formatInternal)(internal, "short", localization, options);
531
+ const longText = (0, format_1.formatInternal)(internal, "long", localization, options);
531
532
  const fhir = (0, fhir_1.toFhir)(internal);
532
533
  if (longText) {
533
534
  fhir.text = longText;
package/dist/parser.js CHANGED
@@ -336,6 +336,7 @@ const EYE_SITE_TOKENS = {
336
336
  };
337
337
  const OPHTHALMIC_ROUTE_CODES = new Set([
338
338
  types_1.RouteCode["Ophthalmic route"],
339
+ types_1.RouteCode["Ocular route (qualifier value)"],
339
340
  types_1.RouteCode["Intravitreal route (qualifier value)"]
340
341
  ]);
341
342
  const OPHTHALMIC_CONTEXT_TOKENS = new Set([
@@ -538,9 +539,13 @@ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
538
539
  const currentToken = tokens[index];
539
540
  const normalizedSelf = normalizeTokenLower(currentToken);
540
541
  const eyeMeta = EYE_SITE_TOKENS[normalizedSelf];
542
+ const contextRoute = (0, context_1.inferRouteFromContext)(context !== null && context !== void 0 ? context : undefined);
541
543
  if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
542
544
  return false;
543
545
  }
546
+ if (contextRoute && !OPHTHALMIC_ROUTE_CODES.has(contextRoute)) {
547
+ return false;
548
+ }
544
549
  if (internal.siteText) {
545
550
  return false;
546
551
  }
@@ -548,7 +553,9 @@ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
548
553
  return false;
549
554
  }
550
555
  const dosageForm = (_a = context === null || context === void 0 ? void 0 : context.dosageForm) === null || _a === void 0 ? void 0 : _a.toLowerCase();
551
- const contextImpliesOphthalmic = Boolean(dosageForm && /(eye|ophth|ocular|intravit)/i.test(dosageForm));
556
+ const contextImpliesOphthalmic = contextRoute
557
+ ? OPHTHALMIC_ROUTE_CODES.has(contextRoute)
558
+ : Boolean(dosageForm && /(eye|ophth|ocular|intravit)/i.test(dosageForm));
552
559
  const eyeRouteImpliesOphthalmic = (eyeMeta === null || eyeMeta === void 0 ? void 0 : eyeMeta.route) === types_1.RouteCode["Intravitreal route (qualifier value)"];
553
560
  const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
554
561
  (internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
@@ -0,0 +1,15 @@
1
+ import { ParsedSigInternal } from "./internal-types";
2
+ import { EventTiming } from "./types";
3
+ export interface TimingSummaryOptions {
4
+ groupMealTimingsByRelation?: boolean;
5
+ includeTimesPerDaySummary?: boolean;
6
+ }
7
+ export type MealRelation = "before" | "after" | "with";
8
+ export type MealName = "breakfast" | "lunch" | "dinner";
9
+ export interface MealTimingGroup {
10
+ relation: MealRelation;
11
+ meals: MealName[];
12
+ codes: EventTiming[];
13
+ }
14
+ export declare function getMealTimingGroup(when: EventTiming[], options?: TimingSummaryOptions): MealTimingGroup | undefined;
15
+ export declare function inferDailyOccurrenceCount(internal: ParsedSigInternal, options?: TimingSummaryOptions): number | undefined;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMealTimingGroup = getMealTimingGroup;
4
+ exports.inferDailyOccurrenceCount = inferDailyOccurrenceCount;
5
+ const types_1 = require("./types");
6
+ const MEAL_TIMING_DETAILS = {
7
+ [types_1.EventTiming["Before Breakfast"]]: { relation: "before", meal: "breakfast" },
8
+ [types_1.EventTiming["Before Lunch"]]: { relation: "before", meal: "lunch" },
9
+ [types_1.EventTiming["Before Dinner"]]: { relation: "before", meal: "dinner" },
10
+ [types_1.EventTiming["After Breakfast"]]: { relation: "after", meal: "breakfast" },
11
+ [types_1.EventTiming["After Lunch"]]: { relation: "after", meal: "lunch" },
12
+ [types_1.EventTiming["After Dinner"]]: { relation: "after", meal: "dinner" },
13
+ [types_1.EventTiming.Breakfast]: { relation: "with", meal: "breakfast" },
14
+ [types_1.EventTiming.Lunch]: { relation: "with", meal: "lunch" },
15
+ [types_1.EventTiming.Dinner]: { relation: "with", meal: "dinner" }
16
+ };
17
+ const MEAL_ORDER = {
18
+ breakfast: 0,
19
+ lunch: 1,
20
+ dinner: 2
21
+ };
22
+ const INFERABLE_DAILY_EVENT_TIMINGS = new Set([
23
+ types_1.EventTiming["Before Sleep"],
24
+ types_1.EventTiming["Before Breakfast"],
25
+ types_1.EventTiming["Before Lunch"],
26
+ types_1.EventTiming["Before Dinner"],
27
+ types_1.EventTiming["After Breakfast"],
28
+ types_1.EventTiming["After Lunch"],
29
+ types_1.EventTiming["After Dinner"],
30
+ types_1.EventTiming.Breakfast,
31
+ types_1.EventTiming.Lunch,
32
+ types_1.EventTiming.Dinner,
33
+ types_1.EventTiming.Morning,
34
+ types_1.EventTiming["Early Morning"],
35
+ types_1.EventTiming["Late Morning"],
36
+ types_1.EventTiming.Noon,
37
+ types_1.EventTiming.Afternoon,
38
+ types_1.EventTiming["Early Afternoon"],
39
+ types_1.EventTiming["Late Afternoon"],
40
+ types_1.EventTiming.Evening,
41
+ types_1.EventTiming["Early Evening"],
42
+ types_1.EventTiming["Late Evening"],
43
+ types_1.EventTiming.Night,
44
+ types_1.EventTiming.Wake,
45
+ types_1.EventTiming["After Sleep"]
46
+ ]);
47
+ function uniqueValues(values) {
48
+ const seen = new Set();
49
+ const result = [];
50
+ for (const value of values) {
51
+ if (!seen.has(value)) {
52
+ seen.add(value);
53
+ result.push(value);
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ function getMealTimingGroup(when, options) {
59
+ if (!(options === null || options === void 0 ? void 0 : options.groupMealTimingsByRelation)) {
60
+ return undefined;
61
+ }
62
+ const uniqueWhen = uniqueValues(when);
63
+ if (uniqueWhen.length < 2) {
64
+ return undefined;
65
+ }
66
+ let relation;
67
+ const meals = [];
68
+ const groupedCodes = [];
69
+ let sawFirstMeal = false;
70
+ for (let i = 0; i < uniqueWhen.length; i += 1) {
71
+ const code = uniqueWhen[i];
72
+ const detail = MEAL_TIMING_DETAILS[code];
73
+ if (!detail) {
74
+ if (sawFirstMeal) {
75
+ break;
76
+ }
77
+ continue;
78
+ }
79
+ if (!sawFirstMeal) {
80
+ sawFirstMeal = true;
81
+ }
82
+ if (!relation) {
83
+ relation = detail.relation;
84
+ }
85
+ else if (relation !== detail.relation) {
86
+ break;
87
+ }
88
+ meals.push(detail.meal);
89
+ groupedCodes.push(code);
90
+ }
91
+ if (groupedCodes.length < 2) {
92
+ return undefined;
93
+ }
94
+ for (let i = 1; i < meals.length; i += 1) {
95
+ const current = meals[i];
96
+ let j = i - 1;
97
+ while (j >= 0 && MEAL_ORDER[meals[j]] > MEAL_ORDER[current]) {
98
+ meals[j + 1] = meals[j];
99
+ j -= 1;
100
+ }
101
+ meals[j + 1] = current;
102
+ }
103
+ if (!relation) {
104
+ return undefined;
105
+ }
106
+ return {
107
+ relation,
108
+ meals,
109
+ codes: groupedCodes
110
+ };
111
+ }
112
+ function inferDailyOccurrenceCount(internal, options) {
113
+ var _a;
114
+ if (!(options === null || options === void 0 ? void 0 : options.includeTimesPerDaySummary)) {
115
+ return undefined;
116
+ }
117
+ if (internal.frequency !== undefined || internal.frequencyMax !== undefined || internal.timingCode) {
118
+ return undefined;
119
+ }
120
+ if (internal.period !== undefined || internal.periodMax !== undefined || internal.periodUnit !== undefined) {
121
+ return undefined;
122
+ }
123
+ if (internal.dayOfWeek.length > 0) {
124
+ return undefined;
125
+ }
126
+ const uniqueWhen = uniqueValues(internal.when);
127
+ for (let i = 0; i < uniqueWhen.length; i += 1) {
128
+ if (!INFERABLE_DAILY_EVENT_TIMINGS.has(uniqueWhen[i])) {
129
+ return undefined;
130
+ }
131
+ }
132
+ const uniqueTimes = uniqueValues((_a = internal.timeOfDay) !== null && _a !== void 0 ? _a : []);
133
+ const occurrences = uniqueWhen.length + uniqueTimes.length;
134
+ if (occurrences === 0) {
135
+ return undefined;
136
+ }
137
+ return occurrences;
138
+ }
package/dist/types.d.ts CHANGED
@@ -296,6 +296,17 @@ export interface MedicationContext {
296
296
  export interface FormatOptions {
297
297
  locale?: "en" | "th" | string;
298
298
  i18n?: SigTranslationConfig;
299
+ /**
300
+ * Collapses repeated meal relation phrases into a grouped phrase when all
301
+ * meal anchors share the same relation (for example, "after breakfast,
302
+ * lunch and dinner" instead of repeating "after" for each meal).
303
+ */
304
+ groupMealTimingsByRelation?: boolean;
305
+ /**
306
+ * Adds a per-day frequency summary when it can be derived safely from the
307
+ * schedule (for example, "three times daily" or "วันละ 3 ครั้ง").
308
+ */
309
+ includeTimesPerDaySummary?: boolean;
299
310
  }
300
311
  export interface FormatBatchOptions extends FormatOptions {
301
312
  /**
@@ -0,0 +1,2 @@
1
+ export declare function enumEntries<T extends Record<string, string | number>>(enumeration: T): Array<[keyof T, T[keyof T]]>;
2
+ export declare function enumValues<T extends Record<string, string | number>>(enumeration: T): Array<T[keyof T]>;
@@ -0,0 +1,7 @@
1
+ import { objectEntries, objectValues } from "./object";
2
+ export function enumEntries(enumeration) {
3
+ return objectEntries(enumeration);
4
+ }
5
+ export function enumValues(enumeration) {
6
+ return objectValues(enumeration);
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",