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 +60 -20
- package/dist/context.js +10 -5
- package/dist/fhir.d.ts +1 -1
- package/dist/fhir.js +40 -32
- package/dist/format.d.ts +3 -2
- package/dist/format.js +371 -86
- package/dist/i18n.d.ts +31 -0
- package/dist/i18n.js +664 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +53 -19
- package/dist/internal-types.d.ts +33 -0
- package/dist/internal-types.js +2 -0
- package/dist/maps.d.ts +2 -0
- package/dist/maps.js +543 -350
- package/dist/package.json +3 -0
- package/dist/parser.d.ts +2 -31
- package/dist/parser.js +809 -142
- package/dist/safety.js +7 -4
- package/dist/schedule.js +148 -76
- package/dist/suggest.js +391 -79
- package/dist/types.d.ts +26 -8
- package/dist/types.js +12 -9
- package/dist/utils/array.d.ts +1 -0
- package/dist/utils/array.js +11 -0
- package/dist/utils/enum.d.ts +2 -0
- package/dist/utils/enum.js +7 -0
- package/dist/utils/object.d.ts +7 -0
- package/dist/utils/object.js +34 -0
- package/package.json +2 -2
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`
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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]
|
|
12
|
+
return (_a = maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE[key]) !== null && _a !== void 0 ? _a : key;
|
|
8
13
|
}
|
|
9
|
-
|
|
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 "./
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
-
? dosage.timing.repeat.when.filter((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
|
|
121
|
-
frequency: dosage.timing
|
|
122
|
-
frequencyMax: dosage.timing
|
|
123
|
-
period: dosage.timing
|
|
124
|
-
periodMax: dosage.timing
|
|
125
|
-
periodUnit: dosage.timing
|
|
126
|
-
routeText: dosage.route
|
|
127
|
-
siteText: dosage.site
|
|
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
|
|
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
|
|
132
|
-
if (routeCoding
|
|
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
|
|
141
|
-
if (doseAndRate
|
|
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
|
|
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
|
|
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
|
|
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 "./
|
|
2
|
-
|
|
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;
|