ezmedicationinput 0.1.1 → 0.1.3

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
@@ -49,6 +49,39 @@ Example output:
49
49
  }
50
50
  ```
51
51
 
52
+ ### Sig (directions) suggestions
53
+
54
+ Use `suggestSig` to drive autocomplete experiences while the clinician is
55
+ typing shorthand medication directions (sig = directions). It returns an array
56
+ of canonical direction strings and accepts the same `ParseOptions` context plus
57
+ a `limit` and custom PRN reasons.
58
+
59
+ ```ts
60
+ import { suggestSig } from "ezmedicationinput";
61
+
62
+ const suggestions = suggestSig("1 drop to od q2h", {
63
+ limit: 5,
64
+ context: { dosageForm: "ophthalmic solution" },
65
+ });
66
+
67
+ // → ["1 drop oph q2h", "1 drop oph q2h prn pain", ...]
68
+ ```
69
+
70
+ Highlights:
71
+
72
+ - Recognizes plural units and their singular counterparts (`tab`/`tabs`,
73
+ `puff`/`puffs`, `mL`/`millilitres`, etc.) and normalizes spelled-out metric,
74
+ SI-prefixed masses/volumes (`micrograms`, `microliters`, `nanograms`,
75
+ `liters`, `kilograms`, etc.) alongside household measures like `teaspoon`
76
+ and `tablespoons` (set `allowHouseholdVolumeUnits: false` to omit them).
77
+ - Keeps matching even when intermediary words such as `to`, `in`, or ocular
78
+ site shorthand (`od`, `os`, `ou`) appear in the prefix.
79
+ - Emits dynamic interval suggestions, including arbitrary `q<number>h` cadences
80
+ and common range patterns like `q4-6h`.
81
+ - Supports multiple timing tokens in sequence (e.g. `1 tab po morn hs`).
82
+ - Surfaces PRN reasons from built-ins or custom `prnReasons` entries while
83
+ preserving numeric doses pulled from the typed prefix.
84
+
52
85
  ## Dictionaries
53
86
 
54
87
  The library exposes default dictionaries in `maps.ts` for routes, units, frequencies (Timing abbreviations + repeat defaults), and event timing tokens. You can extend or override them via the `ParseOptions` argument.
@@ -63,8 +96,12 @@ Key EventTiming mappings include:
63
96
  | `pc breakfast` | `PCM`
64
97
  | `pc lunch` | `PCD`
65
98
  | `pc dinner` | `PCV`
99
+ | `breakfast`, `bfast`, `brkfst`, `brk` | `CM`
100
+ | `lunch`, `lunchtime` | `CD`
101
+ | `dinner`, `dinnertime`, `supper`, `suppertime` | `CV`
66
102
  | `am`, `morning` | `MORN`
67
- | `noon` | `NOON`
103
+ | `noon`, `midday`, `mid-day` | `NOON`
104
+ | `afternoon`, `aft` | `AFT`
68
105
  | `pm`, `evening` | `EVE`
69
106
  | `night` | `NIGHT`
70
107
  | `hs`, `bedtime` | `HS`
@@ -82,11 +119,14 @@ Routes always include SNOMED CT codings. Every code from the SNOMED Route of Adm
82
119
  `null` to explicitly disable context-based inference.
83
120
  - `smartMealExpansion`: when `true`, generic AC/PC/C tokens expand into specific EventTiming combinations (e.g. `1x2 po ac` → `ACM` + `ACV`).
84
121
  - `twoPerDayPair`: controls whether 2× AC/PC/C doses expand to breakfast+dinner (default) or breakfast+lunch.
122
+ - `eventClock`: optional map of `EventTiming` codes to HH:mm strings that drives chronological ordering of parsed `when` values.
123
+ - `allowHouseholdVolumeUnits`: defaults to `true`; set to `false` to ignore
124
+ teaspoon/tablespoon units during parsing and suggestions.
85
125
  - Custom `routeMap`, `unitMap`, `freqMap`, and `whenMap` let you augment the built-in dictionaries without mutating them.
86
126
 
87
127
  ### Next due dose generation
88
128
 
89
- `nextDueDoses` produces upcoming administration timestamps from an existing FHIR `Dosage`. Supply the order start, the reference window, and a clinic configuration that defines anchor times.
129
+ `nextDueDoses` produces upcoming administration timestamps from an existing FHIR `Dosage`. Supply the evaluation window (`from`), optionally the order start (`orderedAt`), and clinic clock details such as a time zone and event timing anchors.
90
130
 
91
131
  ```ts
92
132
  import { EventTiming, nextDueDoses, parseSig } from "ezmedicationinput";
@@ -97,24 +137,22 @@ const schedule = nextDueDoses(fhir, {
97
137
  orderedAt: "2024-01-01T08:15:00Z",
98
138
  from: "2024-01-01T09:00:00Z",
99
139
  limit: 5,
100
- config: {
101
- timeZone: "Asia/Bangkok",
102
- eventClock: {
103
- [EventTiming.Morning]: "08:00",
104
- [EventTiming.Noon]: "12:00",
105
- [EventTiming.Evening]: "18:00",
106
- [EventTiming["Before Sleep"]]: "22:00",
107
- [EventTiming.Breakfast]: "08:00",
108
- [EventTiming.Lunch]: "12:30",
109
- [EventTiming.Dinner]: "18:30"
110
- },
111
- mealOffsets: {
112
- [EventTiming["Before Meal"]]: -30,
113
- [EventTiming["After Meal"]]: 30
114
- },
115
- frequencyDefaults: {
116
- byCode: { BID: ["08:00", "20:00"] }
117
- }
140
+ timeZone: "Asia/Bangkok",
141
+ eventClock: {
142
+ [EventTiming.Morning]: "08:00",
143
+ [EventTiming.Noon]: "12:00",
144
+ [EventTiming.Evening]: "18:00",
145
+ [EventTiming["Before Sleep"]]: "22:00",
146
+ [EventTiming.Breakfast]: "08:00",
147
+ [EventTiming.Lunch]: "12:30",
148
+ [EventTiming.Dinner]: "18:30"
149
+ },
150
+ mealOffsets: {
151
+ [EventTiming["Before Meal"]]: -30,
152
+ [EventTiming["After Meal"]]: 30
153
+ },
154
+ frequencyDefaults: {
155
+ byCode: { BID: ["08:00", "20:00"] }
118
156
  }
119
157
  });
120
158
 
@@ -128,6 +166,8 @@ Key rules:
128
166
  - Pure frequency schedules (`BID`, `TID`, etc.) fall back to clinic-defined institution times.
129
167
  - All timestamps are emitted as ISO strings that include the clinic time-zone offset.
130
168
 
169
+ `from` is required and marks the evaluation window. `orderedAt` is optional—when supplied it acts as the baseline for interval calculations; otherwise the `from` timestamp is reused. The options bag also accepts `timeZone`, `eventClock`, `mealOffsets`, and `frequencyDefaults` at the top level (mirroring the legacy `config` object). `limit` defaults to 10 when omitted.
170
+
131
171
  ### Ocular & intravitreal shortcuts
132
172
 
133
173
  The parser recognizes ophthalmic shorthands such as `OD`, `OS`, `OU`, `LE`, `RE`, and `BE`, as well as intravitreal-specific tokens including `IVT`, `IVTOD`, `IVTOS`, `IVTLE`, `IVTBE`, `VOD`, and `VOS`. Intravitreal sigs require an eye side; the parser surfaces a warning if one is missing so downstream workflows can prompt the clinician for clarification.
package/dist/context.js CHANGED
@@ -1,12 +1,17 @@
1
- import { DEFAULT_UNIT_BY_NORMALIZED_FORM, KNOWN_DOSAGE_FORMS_TO_DOSE } from "./maps";
2
- export function normalizeDosageForm(form) {
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeDosageForm = normalizeDosageForm;
4
+ exports.inferUnitFromContext = inferUnitFromContext;
5
+ const maps_1 = require("./maps");
6
+ function normalizeDosageForm(form) {
7
+ var _a;
3
8
  if (!form) {
4
9
  return undefined;
5
10
  }
6
11
  const key = form.trim().toLowerCase();
7
- return KNOWN_DOSAGE_FORMS_TO_DOSE[key] ?? key;
12
+ return (_a = maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE[key]) !== null && _a !== void 0 ? _a : key;
8
13
  }
9
- export function inferUnitFromContext(ctx) {
14
+ function inferUnitFromContext(ctx) {
10
15
  if (!ctx) {
11
16
  return undefined;
12
17
  }
@@ -16,7 +21,7 @@ export function inferUnitFromContext(ctx) {
16
21
  if (ctx.dosageForm) {
17
22
  const normalized = normalizeDosageForm(ctx.dosageForm);
18
23
  if (normalized) {
19
- const unit = DEFAULT_UNIT_BY_NORMALIZED_FORM[normalized];
24
+ const unit = maps_1.DEFAULT_UNIT_BY_NORMALIZED_FORM[normalized];
20
25
  if (unit) {
21
26
  return unit;
22
27
  }
package/dist/fhir.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ParsedSigInternal } from "./parser";
1
+ import { ParsedSigInternal } from "./internal-types";
2
2
  import { FhirDosage } from "./types";
3
3
  export declare function toFhir(internal: ParsedSigInternal): FhirDosage;
4
4
  export declare function internalFromFhir(dosage: FhirDosage): ParsedSigInternal;
package/dist/fhir.js CHANGED
@@ -1,8 +1,15 @@
1
- import { formatInternal } from "./format";
2
- import { ROUTE_BY_SNOMED, ROUTE_SNOMED, ROUTE_TEXT } from "./maps";
3
- import { EventTiming } from "./types";
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toFhir = toFhir;
4
+ exports.internalFromFhir = internalFromFhir;
5
+ const format_1 = require("./format");
6
+ const maps_1 = require("./maps");
7
+ const types_1 = require("./types");
8
+ const object_1 = require("./utils/object");
9
+ const array_1 = require("./utils/array");
4
10
  const SNOMED_SYSTEM = "http://snomed.info/sct";
5
- export function toFhir(internal) {
11
+ function toFhir(internal) {
12
+ var _a, _b;
6
13
  const dosage = {};
7
14
  const repeat = {};
8
15
  let hasRepeat = false;
@@ -38,7 +45,7 @@ export function toFhir(internal) {
38
45
  dosage.timing = {};
39
46
  }
40
47
  if (internal.timingCode) {
41
- dosage.timing = dosage.timing ?? {};
48
+ dosage.timing = (_a = dosage.timing) !== null && _a !== void 0 ? _a : {};
42
49
  dosage.timing.code = {
43
50
  coding: [{ code: internal.timingCode }],
44
51
  text: internal.timingCode
@@ -70,9 +77,8 @@ export function toFhir(internal) {
70
77
  }
71
78
  // Emit SNOMED-coded routes whenever we have parsed or inferred route data.
72
79
  if (internal.routeCode || internal.routeText) {
73
- const coding = internal.routeCode ? ROUTE_SNOMED[internal.routeCode] : undefined;
74
- const text = internal.routeText ??
75
- (internal.routeCode ? ROUTE_TEXT[internal.routeCode] : undefined);
80
+ const coding = internal.routeCode ? maps_1.ROUTE_SNOMED[internal.routeCode] : undefined;
81
+ const text = (_b = internal.routeText) !== null && _b !== void 0 ? _b : (internal.routeCode ? maps_1.ROUTE_TEXT[internal.routeCode] : undefined);
76
82
  if (coding) {
77
83
  // Provide both text and coding so human-readable and coded systems align.
78
84
  dosage.route = {
@@ -99,53 +105,55 @@ export function toFhir(internal) {
99
105
  dosage.asNeededFor = [{ text: internal.asNeededReason }];
100
106
  }
101
107
  }
102
- const longText = formatInternal(internal, "long");
108
+ const longText = (0, format_1.formatInternal)(internal, "long");
103
109
  if (longText) {
104
110
  dosage.text = longText;
105
111
  }
106
112
  return dosage;
107
113
  }
108
- export function internalFromFhir(dosage) {
114
+ function internalFromFhir(dosage) {
115
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
109
116
  const internal = {
110
- input: dosage.text ?? "",
117
+ input: (_a = dosage.text) !== null && _a !== void 0 ? _a : "",
111
118
  tokens: [],
112
119
  consumed: new Set(),
113
- dayOfWeek: dosage.timing?.repeat?.dayOfWeek
120
+ dayOfWeek: ((_c = (_b = dosage.timing) === null || _b === void 0 ? void 0 : _b.repeat) === null || _c === void 0 ? void 0 : _c.dayOfWeek)
114
121
  ? [...dosage.timing.repeat.dayOfWeek]
115
122
  : [],
116
- when: dosage.timing?.repeat?.when
117
- ? dosage.timing.repeat.when.filter((value) => Object.values(EventTiming).includes(value))
123
+ when: ((_e = (_d = dosage.timing) === null || _d === void 0 ? void 0 : _d.repeat) === null || _e === void 0 ? void 0 : _e.when)
124
+ ? dosage.timing.repeat.when.filter((value) => (0, array_1.arrayIncludes)((0, object_1.objectValues)(types_1.EventTiming), value))
118
125
  : [],
119
126
  warnings: [],
120
- timingCode: dosage.timing?.code?.coding?.[0]?.code,
121
- frequency: dosage.timing?.repeat?.frequency,
122
- frequencyMax: dosage.timing?.repeat?.frequencyMax,
123
- period: dosage.timing?.repeat?.period,
124
- periodMax: dosage.timing?.repeat?.periodMax,
125
- periodUnit: dosage.timing?.repeat?.periodUnit,
126
- routeText: dosage.route?.text,
127
- siteText: dosage.site?.text,
127
+ timingCode: (_j = (_h = (_g = (_f = dosage.timing) === null || _f === void 0 ? void 0 : _f.code) === null || _g === void 0 ? void 0 : _g.coding) === null || _h === void 0 ? void 0 : _h[0]) === null || _j === void 0 ? void 0 : _j.code,
128
+ frequency: (_l = (_k = dosage.timing) === null || _k === void 0 ? void 0 : _k.repeat) === null || _l === void 0 ? void 0 : _l.frequency,
129
+ frequencyMax: (_o = (_m = dosage.timing) === null || _m === void 0 ? void 0 : _m.repeat) === null || _o === void 0 ? void 0 : _o.frequencyMax,
130
+ period: (_q = (_p = dosage.timing) === null || _p === void 0 ? void 0 : _p.repeat) === null || _q === void 0 ? void 0 : _q.period,
131
+ periodMax: (_s = (_r = dosage.timing) === null || _r === void 0 ? void 0 : _r.repeat) === null || _s === void 0 ? void 0 : _s.periodMax,
132
+ periodUnit: (_u = (_t = dosage.timing) === null || _t === void 0 ? void 0 : _t.repeat) === null || _u === void 0 ? void 0 : _u.periodUnit,
133
+ routeText: (_v = dosage.route) === null || _v === void 0 ? void 0 : _v.text,
134
+ siteText: (_w = dosage.site) === null || _w === void 0 ? void 0 : _w.text,
128
135
  asNeeded: dosage.asNeededBoolean,
129
- asNeededReason: dosage.asNeededFor?.[0]?.text
136
+ asNeededReason: (_y = (_x = dosage.asNeededFor) === null || _x === void 0 ? void 0 : _x[0]) === null || _y === void 0 ? void 0 : _y.text,
137
+ siteTokenIndices: new Set()
130
138
  };
131
- const routeCoding = dosage.route?.coding?.find((code) => code.system === SNOMED_SYSTEM);
132
- if (routeCoding?.code) {
139
+ const routeCoding = (_0 = (_z = dosage.route) === null || _z === void 0 ? void 0 : _z.coding) === null || _0 === void 0 ? void 0 : _0.find((code) => code.system === SNOMED_SYSTEM);
140
+ if (routeCoding === null || routeCoding === void 0 ? void 0 : routeCoding.code) {
133
141
  // Translate SNOMED codings back into the simplified enum for round-trip fidelity.
134
- const mapped = ROUTE_BY_SNOMED[routeCoding.code];
142
+ const mapped = maps_1.ROUTE_BY_SNOMED[routeCoding.code];
135
143
  if (mapped) {
136
144
  internal.routeCode = mapped;
137
- internal.routeText = ROUTE_TEXT[mapped];
145
+ internal.routeText = maps_1.ROUTE_TEXT[mapped];
138
146
  }
139
147
  }
140
- const doseAndRate = dosage.doseAndRate?.[0];
141
- if (doseAndRate?.doseRange) {
148
+ const doseAndRate = (_1 = dosage.doseAndRate) === null || _1 === void 0 ? void 0 : _1[0];
149
+ if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseRange) {
142
150
  const { low, high } = doseAndRate.doseRange;
143
- if (low?.value !== undefined && high?.value !== undefined) {
151
+ if ((low === null || low === void 0 ? void 0 : low.value) !== undefined && (high === null || high === void 0 ? void 0 : high.value) !== undefined) {
144
152
  internal.doseRange = { low: low.value, high: high.value };
145
153
  }
146
- internal.unit = low?.unit ?? high?.unit ?? internal.unit;
154
+ internal.unit = (_3 = (_2 = low === null || low === void 0 ? void 0 : low.unit) !== null && _2 !== void 0 ? _2 : high === null || high === void 0 ? void 0 : high.unit) !== null && _3 !== void 0 ? _3 : internal.unit;
147
155
  }
148
- else if (doseAndRate?.doseQuantity) {
156
+ else if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseQuantity) {
149
157
  const dose = doseAndRate.doseQuantity;
150
158
  if (dose.value !== undefined) {
151
159
  internal.dose = dose.value;
package/dist/format.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import { ParsedSigInternal } from "./parser";
2
- export declare function formatInternal(internal: ParsedSigInternal, style: "short" | "long"): string;
1
+ import { ParsedSigInternal } from "./internal-types";
2
+ import type { SigLocalization } from "./i18n";
3
+ export declare function formatInternal(internal: ParsedSigInternal, style: "short" | "long", localization?: SigLocalization): string;