ezmedicationinput 0.1.0
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 +153 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +29 -0
- package/dist/fhir.d.ts +4 -0
- package/dist/fhir.js +158 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.js +322 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +57 -0
- package/dist/maps.d.ts +51 -0
- package/dist/maps.js +729 -0
- package/dist/parser.d.ts +33 -0
- package/dist/parser.js +870 -0
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +12 -0
- package/dist/schedule.d.ts +6 -0
- package/dist/schedule.js +653 -0
- package/dist/types.d.ts +363 -0
- package/dist/types.js +224 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ezmedicationinput
|
|
2
|
+
|
|
3
|
+
`ezmedicationinput` parses concise clinician shorthand medication instructions and produces [FHIR R5 Dosage](https://hl7.org/fhir/dosage.html) JSON. It is designed for use in lightweight medication-entry experiences and ships with batteries-included normalization for common Thai/English sig abbreviations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Converts shorthand strings (e.g. `1x3 po pc`, `500 mg po q6h prn pain`) into FHIR-compliant dosage JSON.
|
|
8
|
+
- Emits timing abbreviations (`timing.code`) and repeat structures simultaneously where possible.
|
|
9
|
+
- Maps meal/time blocks to the correct `Timing.repeat.when` **EventTiming** codes and can auto-expand AC/PC/C into specific meals.
|
|
10
|
+
- Outputs SNOMED CT route codings (while providing friendly text) and round-trips known SNOMED routes back into the parser.
|
|
11
|
+
- Understands ocular and intravitreal shorthand (OD/OS/OU, LE/RE/BE, IVT*, VOD/VOS, etc.) and warns when intravitreal instructions omit an eye side.
|
|
12
|
+
- Parses fractional/ minute-based intervals (`q0.5h`, `q30 min`, `q1/4hr`) plus dose and timing ranges.
|
|
13
|
+
- Supports extensible dictionaries for routes, units, frequency shorthands, and event timing tokens.
|
|
14
|
+
- Applies medication context to infer default units when they are omitted.
|
|
15
|
+
- Surfaces warnings when discouraged tokens (`QD`, `QOD`, `BLD`) are used and optionally rejects them.
|
|
16
|
+
- Generates upcoming administration timestamps from FHIR dosage data via `nextDueDoses` using configurable clinic clocks.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install ezmedicationinput
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { parseSig } from "ezmedicationinput";
|
|
28
|
+
|
|
29
|
+
const result = parseSig("1x3 po pc", { context: { dosageForm: "tab" } });
|
|
30
|
+
console.log(result.fhir);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Example output:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"text": "1 tablet by mouth three times daily after meals",
|
|
38
|
+
"timing": {
|
|
39
|
+
"code": { "coding": [{ "code": "TID" }], "text": "TID" },
|
|
40
|
+
"repeat": {
|
|
41
|
+
"frequency": 3,
|
|
42
|
+
"period": 1,
|
|
43
|
+
"periodUnit": "d",
|
|
44
|
+
"when": ["PC"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"route": { "text": "by mouth" },
|
|
48
|
+
"doseAndRate": [{ "doseQuantity": { "value": 1, "unit": "tab" } }]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Dictionaries
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
Key EventTiming mappings include:
|
|
57
|
+
|
|
58
|
+
| Token(s) | EventTiming |
|
|
59
|
+
|-----------------|-------------|
|
|
60
|
+
| `ac` | `AC`
|
|
61
|
+
| `pc` | `PC`
|
|
62
|
+
| `wm`, `with meals` | `C`
|
|
63
|
+
| `pc breakfast` | `PCM`
|
|
64
|
+
| `pc lunch` | `PCD`
|
|
65
|
+
| `pc dinner` | `PCV`
|
|
66
|
+
| `am`, `morning` | `MORN`
|
|
67
|
+
| `noon` | `NOON`
|
|
68
|
+
| `pm`, `evening` | `EVE`
|
|
69
|
+
| `night` | `NIGHT`
|
|
70
|
+
| `hs`, `bedtime` | `HS`
|
|
71
|
+
|
|
72
|
+
When `when` is populated, `timeOfDay` is intentionally omitted to stay within HL7 constraints.
|
|
73
|
+
|
|
74
|
+
Routes always include SNOMED CT codings. Every code from the SNOMED Route of Administration value set is represented so you can confidently pass parsed results into downstream FHIR services that expect coded routes.
|
|
75
|
+
|
|
76
|
+
### Advanced parsing options
|
|
77
|
+
|
|
78
|
+
`parseSig` accepts a `ParseOptions` object. Highlights:
|
|
79
|
+
|
|
80
|
+
- `context`: optional medication context (dosage form, strength, container
|
|
81
|
+
metadata) used to infer default units when a sig omits explicit units. Pass
|
|
82
|
+
`null` to explicitly disable context-based inference.
|
|
83
|
+
- `smartMealExpansion`: when `true`, generic AC/PC/C tokens expand into specific EventTiming combinations (e.g. `1x2 po ac` → `ACM` + `ACV`).
|
|
84
|
+
- `twoPerDayPair`: controls whether 2× AC/PC/C doses expand to breakfast+dinner (default) or breakfast+lunch.
|
|
85
|
+
- Custom `routeMap`, `unitMap`, `freqMap`, and `whenMap` let you augment the built-in dictionaries without mutating them.
|
|
86
|
+
|
|
87
|
+
### Next due dose generation
|
|
88
|
+
|
|
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.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { EventTiming, nextDueDoses, parseSig } from "ezmedicationinput";
|
|
93
|
+
|
|
94
|
+
const { fhir } = parseSig("1x3 po pc", { context: { dosageForm: "tab" } });
|
|
95
|
+
|
|
96
|
+
const schedule = nextDueDoses(fhir, {
|
|
97
|
+
orderedAt: "2024-01-01T08:15:00Z",
|
|
98
|
+
from: "2024-01-01T09:00:00Z",
|
|
99
|
+
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// → ["2024-01-01T12:30:00+07:00", "2024-01-01T18:30:00+07:00", ...]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Key rules:
|
|
125
|
+
|
|
126
|
+
- `when` values map to the clinic `eventClock`. Generic meal codes (`AC`, `PC`, `C`) use `mealOffsets` against breakfast/lunch/dinner anchors.
|
|
127
|
+
- Interval-based schedules (`repeat.period` + `periodUnit`) step forward from `orderedAt`, respecting `dayOfWeek` filters.
|
|
128
|
+
- Pure frequency schedules (`BID`, `TID`, etc.) fall back to clinic-defined institution times.
|
|
129
|
+
- All timestamps are emitted as ISO strings that include the clinic time-zone offset.
|
|
130
|
+
|
|
131
|
+
### Ocular & intravitreal shortcuts
|
|
132
|
+
|
|
133
|
+
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.
|
|
134
|
+
|
|
135
|
+
## Discouraged Tokens
|
|
136
|
+
|
|
137
|
+
- `QD` (daily)
|
|
138
|
+
- `QOD` (every other day)
|
|
139
|
+
- `BLD` / `B-L-D` (with meals)
|
|
140
|
+
|
|
141
|
+
By default these are accepted with a warning via `ParseResult.warnings`. Set `allowDiscouraged: false` in `ParseOptions` to reject inputs containing them.
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
Run the Vitest test suite:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm test
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DEFAULT_UNIT_BY_NORMALIZED_FORM, KNOWN_DOSAGE_FORMS_TO_DOSE } from "./maps";
|
|
2
|
+
export function normalizeDosageForm(form) {
|
|
3
|
+
if (!form) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const key = form.trim().toLowerCase();
|
|
7
|
+
return KNOWN_DOSAGE_FORMS_TO_DOSE[key] ?? key;
|
|
8
|
+
}
|
|
9
|
+
export function inferUnitFromContext(ctx) {
|
|
10
|
+
if (!ctx) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (ctx.defaultUnit) {
|
|
14
|
+
return ctx.defaultUnit;
|
|
15
|
+
}
|
|
16
|
+
if (ctx.dosageForm) {
|
|
17
|
+
const normalized = normalizeDosageForm(ctx.dosageForm);
|
|
18
|
+
if (normalized) {
|
|
19
|
+
const unit = DEFAULT_UNIT_BY_NORMALIZED_FORM[normalized];
|
|
20
|
+
if (unit) {
|
|
21
|
+
return unit;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (ctx.containerUnit) {
|
|
26
|
+
return ctx.containerUnit;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
package/dist/fhir.d.ts
ADDED
package/dist/fhir.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { formatInternal } from "./format";
|
|
2
|
+
import { ROUTE_BY_SNOMED, ROUTE_SNOMED, ROUTE_TEXT } from "./maps";
|
|
3
|
+
import { EventTiming } from "./types";
|
|
4
|
+
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
5
|
+
export function toFhir(internal) {
|
|
6
|
+
const dosage = {};
|
|
7
|
+
const repeat = {};
|
|
8
|
+
let hasRepeat = false;
|
|
9
|
+
if (internal.frequency !== undefined) {
|
|
10
|
+
repeat.frequency = internal.frequency;
|
|
11
|
+
hasRepeat = true;
|
|
12
|
+
}
|
|
13
|
+
if (internal.frequencyMax !== undefined) {
|
|
14
|
+
repeat.frequencyMax = internal.frequencyMax;
|
|
15
|
+
hasRepeat = true;
|
|
16
|
+
}
|
|
17
|
+
if (internal.period !== undefined && internal.periodUnit) {
|
|
18
|
+
repeat.period = internal.period;
|
|
19
|
+
repeat.periodUnit = internal.periodUnit;
|
|
20
|
+
hasRepeat = true;
|
|
21
|
+
}
|
|
22
|
+
if (internal.periodMax !== undefined) {
|
|
23
|
+
repeat.periodMax = internal.periodMax;
|
|
24
|
+
hasRepeat = true;
|
|
25
|
+
}
|
|
26
|
+
if (internal.dayOfWeek.length) {
|
|
27
|
+
repeat.dayOfWeek = [...internal.dayOfWeek];
|
|
28
|
+
hasRepeat = true;
|
|
29
|
+
}
|
|
30
|
+
if (internal.when.length) {
|
|
31
|
+
repeat.when = [...internal.when];
|
|
32
|
+
hasRepeat = true;
|
|
33
|
+
}
|
|
34
|
+
if (hasRepeat) {
|
|
35
|
+
dosage.timing = { repeat };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
dosage.timing = {};
|
|
39
|
+
}
|
|
40
|
+
if (internal.timingCode) {
|
|
41
|
+
dosage.timing = dosage.timing ?? {};
|
|
42
|
+
dosage.timing.code = {
|
|
43
|
+
coding: [{ code: internal.timingCode }],
|
|
44
|
+
text: internal.timingCode
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (internal.doseRange) {
|
|
48
|
+
dosage.doseAndRate = [
|
|
49
|
+
{
|
|
50
|
+
doseRange: {
|
|
51
|
+
low: internal.doseRange.low !== undefined
|
|
52
|
+
? { value: internal.doseRange.low, unit: internal.unit }
|
|
53
|
+
: undefined,
|
|
54
|
+
high: internal.doseRange.high !== undefined
|
|
55
|
+
? { value: internal.doseRange.high, unit: internal.unit }
|
|
56
|
+
: undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
else if (internal.dose !== undefined) {
|
|
62
|
+
dosage.doseAndRate = [
|
|
63
|
+
{
|
|
64
|
+
doseQuantity: {
|
|
65
|
+
value: internal.dose,
|
|
66
|
+
unit: internal.unit
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
// Emit SNOMED-coded routes whenever we have parsed or inferred route data.
|
|
72
|
+
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);
|
|
76
|
+
if (coding) {
|
|
77
|
+
// Provide both text and coding so human-readable and coded systems align.
|
|
78
|
+
dosage.route = {
|
|
79
|
+
text,
|
|
80
|
+
coding: [
|
|
81
|
+
{
|
|
82
|
+
system: SNOMED_SYSTEM,
|
|
83
|
+
code: coding.code,
|
|
84
|
+
display: coding.display
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
else if (text) {
|
|
90
|
+
dosage.route = { text };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (internal.siteText) {
|
|
94
|
+
dosage.site = { text: internal.siteText };
|
|
95
|
+
}
|
|
96
|
+
if (internal.asNeeded) {
|
|
97
|
+
dosage.asNeededBoolean = true;
|
|
98
|
+
if (internal.asNeededReason) {
|
|
99
|
+
dosage.asNeededFor = [{ text: internal.asNeededReason }];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const longText = formatInternal(internal, "long");
|
|
103
|
+
if (longText) {
|
|
104
|
+
dosage.text = longText;
|
|
105
|
+
}
|
|
106
|
+
return dosage;
|
|
107
|
+
}
|
|
108
|
+
export function internalFromFhir(dosage) {
|
|
109
|
+
const internal = {
|
|
110
|
+
input: dosage.text ?? "",
|
|
111
|
+
tokens: [],
|
|
112
|
+
consumed: new Set(),
|
|
113
|
+
dayOfWeek: dosage.timing?.repeat?.dayOfWeek
|
|
114
|
+
? [...dosage.timing.repeat.dayOfWeek]
|
|
115
|
+
: [],
|
|
116
|
+
when: dosage.timing?.repeat?.when
|
|
117
|
+
? dosage.timing.repeat.when.filter((value) => Object.values(EventTiming).includes(value))
|
|
118
|
+
: [],
|
|
119
|
+
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,
|
|
128
|
+
asNeeded: dosage.asNeededBoolean,
|
|
129
|
+
asNeededReason: dosage.asNeededFor?.[0]?.text
|
|
130
|
+
};
|
|
131
|
+
const routeCoding = dosage.route?.coding?.find((code) => code.system === SNOMED_SYSTEM);
|
|
132
|
+
if (routeCoding?.code) {
|
|
133
|
+
// Translate SNOMED codings back into the simplified enum for round-trip fidelity.
|
|
134
|
+
const mapped = ROUTE_BY_SNOMED[routeCoding.code];
|
|
135
|
+
if (mapped) {
|
|
136
|
+
internal.routeCode = mapped;
|
|
137
|
+
internal.routeText = ROUTE_TEXT[mapped];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const doseAndRate = dosage.doseAndRate?.[0];
|
|
141
|
+
if (doseAndRate?.doseRange) {
|
|
142
|
+
const { low, high } = doseAndRate.doseRange;
|
|
143
|
+
if (low?.value !== undefined && high?.value !== undefined) {
|
|
144
|
+
internal.doseRange = { low: low.value, high: high.value };
|
|
145
|
+
}
|
|
146
|
+
internal.unit = low?.unit ?? high?.unit ?? internal.unit;
|
|
147
|
+
}
|
|
148
|
+
else if (doseAndRate?.doseQuantity) {
|
|
149
|
+
const dose = doseAndRate.doseQuantity;
|
|
150
|
+
if (dose.value !== undefined) {
|
|
151
|
+
internal.dose = dose.value;
|
|
152
|
+
}
|
|
153
|
+
if (dose.unit) {
|
|
154
|
+
internal.unit = dose.unit;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return internal;
|
|
158
|
+
}
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { EventTiming, FhirPeriodUnit, RouteCode } from "./types";
|
|
2
|
+
const ROUTE_SHORT = {
|
|
3
|
+
[RouteCode["Oral route"]]: "PO",
|
|
4
|
+
[RouteCode["Sublingual route"]]: "SL",
|
|
5
|
+
[RouteCode["Buccal route"]]: "BUC",
|
|
6
|
+
[RouteCode["Respiratory tract route (qualifier value)"]]: "INH",
|
|
7
|
+
[RouteCode["Nasal route"]]: "IN",
|
|
8
|
+
[RouteCode["Topical route"]]: "TOP",
|
|
9
|
+
[RouteCode["Transdermal route"]]: "TD",
|
|
10
|
+
[RouteCode["Subcutaneous route"]]: "SC",
|
|
11
|
+
[RouteCode["Intramuscular route"]]: "IM",
|
|
12
|
+
[RouteCode["Intravenous route"]]: "IV",
|
|
13
|
+
[RouteCode["Per rectum"]]: "PR",
|
|
14
|
+
[RouteCode["Per vagina"]]: "PV",
|
|
15
|
+
[RouteCode["Ophthalmic route"]]: "OPH",
|
|
16
|
+
[RouteCode["Intravitreal route (qualifier value)"]]: "IVT"
|
|
17
|
+
};
|
|
18
|
+
const WHEN_TEXT = {
|
|
19
|
+
[EventTiming["Before Sleep"]]: "at bedtime",
|
|
20
|
+
[EventTiming["Before Meal"]]: "before meals",
|
|
21
|
+
[EventTiming["Before Breakfast"]]: "before breakfast",
|
|
22
|
+
[EventTiming["Before Lunch"]]: "before lunch",
|
|
23
|
+
[EventTiming["Before Dinner"]]: "before dinner",
|
|
24
|
+
[EventTiming["After Meal"]]: "after meals",
|
|
25
|
+
[EventTiming["After Breakfast"]]: "after breakfast",
|
|
26
|
+
[EventTiming["After Lunch"]]: "after lunch",
|
|
27
|
+
[EventTiming["After Dinner"]]: "after dinner",
|
|
28
|
+
[EventTiming.Meal]: "with meals",
|
|
29
|
+
[EventTiming.Breakfast]: "with morning meal",
|
|
30
|
+
[EventTiming.Lunch]: "with lunch",
|
|
31
|
+
[EventTiming.Dinner]: "with evening meal",
|
|
32
|
+
[EventTiming.Morning]: "in the morning",
|
|
33
|
+
[EventTiming["Early Morning"]]: "in the early morning",
|
|
34
|
+
[EventTiming["Late Morning"]]: "in the late morning",
|
|
35
|
+
[EventTiming.Noon]: "at noon",
|
|
36
|
+
[EventTiming.Afternoon]: "in the afternoon",
|
|
37
|
+
[EventTiming["Early Afternoon"]]: "in the early afternoon",
|
|
38
|
+
[EventTiming["Late Afternoon"]]: "in the late afternoon",
|
|
39
|
+
[EventTiming.Evening]: "in the evening",
|
|
40
|
+
[EventTiming["Early Evening"]]: "in the early evening",
|
|
41
|
+
[EventTiming["Late Evening"]]: "in the late evening",
|
|
42
|
+
[EventTiming.Night]: "at night",
|
|
43
|
+
[EventTiming.Wake]: "after waking",
|
|
44
|
+
[EventTiming["After Sleep"]]: "after sleep",
|
|
45
|
+
[EventTiming.Immediate]: "immediately"
|
|
46
|
+
};
|
|
47
|
+
const DAY_NAMES = {
|
|
48
|
+
mon: "Monday",
|
|
49
|
+
tue: "Tuesday",
|
|
50
|
+
wed: "Wednesday",
|
|
51
|
+
thu: "Thursday",
|
|
52
|
+
fri: "Friday",
|
|
53
|
+
sat: "Saturday",
|
|
54
|
+
sun: "Sunday"
|
|
55
|
+
};
|
|
56
|
+
function pluralize(unit, value) {
|
|
57
|
+
if (Math.abs(value) === 1) {
|
|
58
|
+
if (unit === "tab")
|
|
59
|
+
return "tablet";
|
|
60
|
+
if (unit === "cap")
|
|
61
|
+
return "capsule";
|
|
62
|
+
return unit;
|
|
63
|
+
}
|
|
64
|
+
if (unit === "tab" || unit === "tablet")
|
|
65
|
+
return "tablets";
|
|
66
|
+
if (unit === "cap" || unit === "capsule")
|
|
67
|
+
return "capsules";
|
|
68
|
+
if (unit === "mL")
|
|
69
|
+
return "mL";
|
|
70
|
+
if (unit === "mg")
|
|
71
|
+
return "mg";
|
|
72
|
+
if (unit === "puff")
|
|
73
|
+
return value === 1 ? "puff" : "puffs";
|
|
74
|
+
if (unit === "patch")
|
|
75
|
+
return value === 1 ? "patch" : "patches";
|
|
76
|
+
if (unit === "drop")
|
|
77
|
+
return value === 1 ? "drop" : "drops";
|
|
78
|
+
if (unit === "suppository")
|
|
79
|
+
return value === 1 ? "suppository" : "suppositories";
|
|
80
|
+
return unit;
|
|
81
|
+
}
|
|
82
|
+
function describeFrequency(internal) {
|
|
83
|
+
const { frequency, frequencyMax, period, periodMax, periodUnit, timingCode } = internal;
|
|
84
|
+
if (frequency !== undefined &&
|
|
85
|
+
frequencyMax !== undefined &&
|
|
86
|
+
periodUnit === FhirPeriodUnit.Day &&
|
|
87
|
+
(!period || period === 1)) {
|
|
88
|
+
if (frequency === 1 && frequencyMax === 1) {
|
|
89
|
+
return "once daily";
|
|
90
|
+
}
|
|
91
|
+
if (frequency === 1 && frequencyMax === 2) {
|
|
92
|
+
return "one to two times daily";
|
|
93
|
+
}
|
|
94
|
+
return `${stripTrailingZero(frequency)} to ${stripTrailingZero(frequencyMax)} times daily`;
|
|
95
|
+
}
|
|
96
|
+
if (frequency && periodUnit === FhirPeriodUnit.Day && (!period || period === 1)) {
|
|
97
|
+
if (frequency === 1)
|
|
98
|
+
return "once daily";
|
|
99
|
+
if (frequency === 2)
|
|
100
|
+
return "twice daily";
|
|
101
|
+
if (frequency === 3)
|
|
102
|
+
return "three times daily";
|
|
103
|
+
if (frequency === 4)
|
|
104
|
+
return "four times daily";
|
|
105
|
+
return `${stripTrailingZero(frequency)} times daily`;
|
|
106
|
+
}
|
|
107
|
+
if (periodUnit === FhirPeriodUnit.Hour && period) {
|
|
108
|
+
if (periodMax && periodMax !== period) {
|
|
109
|
+
return `every ${stripTrailingZero(period)} to ${stripTrailingZero(periodMax)} hours`;
|
|
110
|
+
}
|
|
111
|
+
return `every ${stripTrailingZero(period)} hour${period === 1 ? "" : "s"}`;
|
|
112
|
+
}
|
|
113
|
+
if (periodUnit === FhirPeriodUnit.Day && period && period !== 1) {
|
|
114
|
+
if (period === 2 && (!periodMax || periodMax === 2)) {
|
|
115
|
+
return "every other day";
|
|
116
|
+
}
|
|
117
|
+
if (periodMax && periodMax !== period) {
|
|
118
|
+
return `every ${stripTrailingZero(period)} to ${stripTrailingZero(periodMax)} days`;
|
|
119
|
+
}
|
|
120
|
+
return `every ${stripTrailingZero(period)} days`;
|
|
121
|
+
}
|
|
122
|
+
if (periodUnit === FhirPeriodUnit.Week && period) {
|
|
123
|
+
if (period === 1 && (!periodMax || periodMax === 1)) {
|
|
124
|
+
return "once weekly";
|
|
125
|
+
}
|
|
126
|
+
if (periodMax && periodMax !== period) {
|
|
127
|
+
return `every ${stripTrailingZero(period)} to ${stripTrailingZero(periodMax)} weeks`;
|
|
128
|
+
}
|
|
129
|
+
return `every ${stripTrailingZero(period)} weeks`;
|
|
130
|
+
}
|
|
131
|
+
if (periodUnit === FhirPeriodUnit.Month && period) {
|
|
132
|
+
if (period === 1 && (!periodMax || periodMax === 1)) {
|
|
133
|
+
return "once monthly";
|
|
134
|
+
}
|
|
135
|
+
if (periodMax && periodMax !== period) {
|
|
136
|
+
return `every ${stripTrailingZero(period)} to ${stripTrailingZero(periodMax)} months`;
|
|
137
|
+
}
|
|
138
|
+
return `every ${stripTrailingZero(period)} months`;
|
|
139
|
+
}
|
|
140
|
+
if (timingCode) {
|
|
141
|
+
if (timingCode === "WK") {
|
|
142
|
+
return "once weekly";
|
|
143
|
+
}
|
|
144
|
+
if (timingCode === "MO") {
|
|
145
|
+
return "once monthly";
|
|
146
|
+
}
|
|
147
|
+
const map = {
|
|
148
|
+
BID: "twice daily",
|
|
149
|
+
TID: "three times daily",
|
|
150
|
+
QID: "four times daily",
|
|
151
|
+
QD: "once daily",
|
|
152
|
+
QOD: "every other day",
|
|
153
|
+
Q6H: "every 6 hours",
|
|
154
|
+
Q8H: "every 8 hours"
|
|
155
|
+
};
|
|
156
|
+
if (map[timingCode]) {
|
|
157
|
+
return map[timingCode];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (frequency && periodUnit === undefined && period === undefined) {
|
|
161
|
+
if (frequency === 1)
|
|
162
|
+
return "once";
|
|
163
|
+
return `${stripTrailingZero(frequency)} times`;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
function formatDoseShort(internal) {
|
|
168
|
+
if (internal.doseRange) {
|
|
169
|
+
const { low, high } = internal.doseRange;
|
|
170
|
+
const base = `${stripTrailingZero(low)}-${stripTrailingZero(high)}`;
|
|
171
|
+
if (internal.unit) {
|
|
172
|
+
return `${base} ${internal.unit}`;
|
|
173
|
+
}
|
|
174
|
+
return base;
|
|
175
|
+
}
|
|
176
|
+
if (internal.dose !== undefined) {
|
|
177
|
+
const dosePart = internal.unit
|
|
178
|
+
? `${stripTrailingZero(internal.dose)} ${internal.unit}`
|
|
179
|
+
: `${stripTrailingZero(internal.dose)}`;
|
|
180
|
+
return dosePart.trim();
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function formatDoseLong(internal) {
|
|
185
|
+
if (internal.doseRange) {
|
|
186
|
+
const { low, high } = internal.doseRange;
|
|
187
|
+
if (internal.unit) {
|
|
188
|
+
return `${stripTrailingZero(low)} to ${stripTrailingZero(high)} ${pluralize(internal.unit, high)}`;
|
|
189
|
+
}
|
|
190
|
+
return `${stripTrailingZero(low)} to ${stripTrailingZero(high)}`;
|
|
191
|
+
}
|
|
192
|
+
if (internal.dose !== undefined) {
|
|
193
|
+
if (internal.unit) {
|
|
194
|
+
return `${stripTrailingZero(internal.dose)} ${pluralize(internal.unit, internal.dose)}`;
|
|
195
|
+
}
|
|
196
|
+
return `${stripTrailingZero(internal.dose)}`;
|
|
197
|
+
}
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
function describeWhen(internal) {
|
|
201
|
+
if (!internal.when.length) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const parts = internal.when
|
|
205
|
+
.map((code) => WHEN_TEXT[code] ?? code)
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
if (!parts.length) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return parts.join(" and ");
|
|
211
|
+
}
|
|
212
|
+
function describeDayOfWeek(internal) {
|
|
213
|
+
if (!internal.dayOfWeek.length) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const days = internal.dayOfWeek.map((d) => DAY_NAMES[d] ?? d);
|
|
217
|
+
if (days.length === 1) {
|
|
218
|
+
return `on ${days[0]}`;
|
|
219
|
+
}
|
|
220
|
+
return `on ${days.join(" and ")}`;
|
|
221
|
+
}
|
|
222
|
+
export function formatInternal(internal, style) {
|
|
223
|
+
if (style === "short") {
|
|
224
|
+
return formatShort(internal);
|
|
225
|
+
}
|
|
226
|
+
return formatLong(internal);
|
|
227
|
+
}
|
|
228
|
+
function formatShort(internal) {
|
|
229
|
+
const parts = [];
|
|
230
|
+
const dosePart = formatDoseShort(internal);
|
|
231
|
+
if (dosePart) {
|
|
232
|
+
parts.push(dosePart);
|
|
233
|
+
}
|
|
234
|
+
if (internal.routeCode) {
|
|
235
|
+
const short = ROUTE_SHORT[internal.routeCode];
|
|
236
|
+
if (short) {
|
|
237
|
+
parts.push(short);
|
|
238
|
+
}
|
|
239
|
+
else if (internal.routeText) {
|
|
240
|
+
parts.push(internal.routeText);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (internal.routeText) {
|
|
244
|
+
parts.push(internal.routeText);
|
|
245
|
+
}
|
|
246
|
+
if (internal.timingCode) {
|
|
247
|
+
parts.push(internal.timingCode);
|
|
248
|
+
}
|
|
249
|
+
else if (internal.frequency !== undefined &&
|
|
250
|
+
internal.frequencyMax !== undefined &&
|
|
251
|
+
internal.periodUnit === FhirPeriodUnit.Day &&
|
|
252
|
+
(!internal.period || internal.period === 1)) {
|
|
253
|
+
parts.push(`${stripTrailingZero(internal.frequency)}-${stripTrailingZero(internal.frequencyMax)}x/d`);
|
|
254
|
+
}
|
|
255
|
+
else if (internal.frequency &&
|
|
256
|
+
internal.periodUnit === FhirPeriodUnit.Day &&
|
|
257
|
+
(!internal.period || internal.period === 1)) {
|
|
258
|
+
parts.push(`${stripTrailingZero(internal.frequency)}x/d`);
|
|
259
|
+
}
|
|
260
|
+
else if (internal.period && internal.periodUnit) {
|
|
261
|
+
const base = stripTrailingZero(internal.period);
|
|
262
|
+
const qualifier = internal.periodMax && internal.periodMax !== internal.period
|
|
263
|
+
? `${base}-${stripTrailingZero(internal.periodMax)}`
|
|
264
|
+
: base;
|
|
265
|
+
parts.push(`Q${qualifier}${internal.periodUnit.toUpperCase()}`);
|
|
266
|
+
}
|
|
267
|
+
if (internal.when.length) {
|
|
268
|
+
parts.push(internal.when.join(" "));
|
|
269
|
+
}
|
|
270
|
+
if (internal.dayOfWeek.length) {
|
|
271
|
+
parts.push(internal.dayOfWeek
|
|
272
|
+
.map((d) => d.charAt(0).toUpperCase() + d.slice(1, 3))
|
|
273
|
+
.join(","));
|
|
274
|
+
}
|
|
275
|
+
if (internal.asNeeded) {
|
|
276
|
+
if (internal.asNeededReason) {
|
|
277
|
+
parts.push(`PRN ${internal.asNeededReason}`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
parts.push("PRN");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return parts.filter(Boolean).join(" ");
|
|
284
|
+
}
|
|
285
|
+
function formatLong(internal) {
|
|
286
|
+
const parts = [];
|
|
287
|
+
const dosePart = formatDoseLong(internal);
|
|
288
|
+
if (dosePart) {
|
|
289
|
+
parts.push(dosePart);
|
|
290
|
+
}
|
|
291
|
+
if (internal.routeText) {
|
|
292
|
+
parts.push(internal.routeText);
|
|
293
|
+
}
|
|
294
|
+
const freqText = describeFrequency(internal);
|
|
295
|
+
if (freqText) {
|
|
296
|
+
parts.push(freqText);
|
|
297
|
+
}
|
|
298
|
+
const whenText = describeWhen(internal);
|
|
299
|
+
if (whenText) {
|
|
300
|
+
parts.push(whenText);
|
|
301
|
+
}
|
|
302
|
+
const dayText = describeDayOfWeek(internal);
|
|
303
|
+
if (dayText) {
|
|
304
|
+
parts.push(dayText);
|
|
305
|
+
}
|
|
306
|
+
if (internal.asNeeded) {
|
|
307
|
+
parts.push(internal.asNeededReason
|
|
308
|
+
? `as needed for ${internal.asNeededReason}`
|
|
309
|
+
: "as needed");
|
|
310
|
+
}
|
|
311
|
+
if (internal.siteText) {
|
|
312
|
+
parts.push(`at ${internal.siteText}`);
|
|
313
|
+
}
|
|
314
|
+
return parts.join(" ").trim();
|
|
315
|
+
}
|
|
316
|
+
function stripTrailingZero(value) {
|
|
317
|
+
const text = value.toString();
|
|
318
|
+
if (text.includes(".")) {
|
|
319
|
+
return text.replace(/\.0+$/, "").replace(/0+$/, "");
|
|
320
|
+
}
|
|
321
|
+
return text;
|
|
322
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FhirDosage, ParseOptions, ParseResult } from "./types";
|
|
2
|
+
export { parseInternal } from "./parser";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export { nextDueDoses } from "./schedule";
|
|
5
|
+
export declare function parseSig(input: string, options?: ParseOptions): ParseResult;
|
|
6
|
+
export declare function formatSig(dosage: FhirDosage, style?: "short" | "long"): string;
|
|
7
|
+
export declare function fromFhirDosage(dosage: FhirDosage): ParseResult;
|