ezmedicationinput 0.1.23 → 0.1.25
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 +52 -0
- package/dist/fhir.js +34 -27
- package/dist/format.js +16 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -1
- package/dist/internal-types.d.ts +1 -0
- package/dist/parser.d.ts +5 -1
- package/dist/parser.js +232 -20
- package/dist/schedule.d.ts +2 -1
- package/dist/schedule.js +209 -2
- package/dist/types.d.ts +33 -0
- package/dist/utils/strength.d.ts +12 -0
- package/dist/utils/strength.js +149 -0
- package/dist/utils/units.d.ts +14 -0
- package/dist/utils/units.js +82 -0
- package/package.json +1 -1
- package/dist/utils/enum.d.ts +0 -2
- package/dist/utils/enum.js +0 -7
package/README.md
CHANGED
|
@@ -352,6 +352,58 @@ Key rules:
|
|
|
352
352
|
|
|
353
353
|
`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.
|
|
354
354
|
|
|
355
|
+
### Medication amount calculation
|
|
356
|
+
|
|
357
|
+
`calculateTotalUnits` computes the total amount of medication (and optionally the number of containers) required for a specific duration. It accounts for complex schedules, dose ranges (using the high value), and unit conversions between doses and containers.
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { calculateTotalUnits, parseSig } from "ezmedicationinput";
|
|
361
|
+
|
|
362
|
+
const { fhir } = parseSig("1x3 po pc");
|
|
363
|
+
|
|
364
|
+
const result = calculateTotalUnits({
|
|
365
|
+
dosage: fhir,
|
|
366
|
+
from: "2024-01-01T08:00:00Z",
|
|
367
|
+
durationValue: 7,
|
|
368
|
+
durationUnit: "d",
|
|
369
|
+
timeZone: "Asia/Bangkok",
|
|
370
|
+
context: {
|
|
371
|
+
containerValue: 30, // 30 tabs per bottle
|
|
372
|
+
containerUnit: "tab"
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// → { totalUnits: 21, totalContainers: 1 }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
It can also handle strength-based conversions (e.g. calculating how many 100mL bottles are needed for a 500mg TID dose of a 250mg/5mL suspension).
|
|
380
|
+
|
|
381
|
+
### Strength parsing
|
|
382
|
+
|
|
383
|
+
Use `parseStrength` to normalize medication strength strings into FHIR-compliant **Quantity** or **Ratio** structures. It understands percentages, ratios, and composite strengths.
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { parseStrength } from "ezmedicationinput";
|
|
387
|
+
|
|
388
|
+
// Percentage (infers g/100mL for liquids or g/100g for solids)
|
|
389
|
+
parseStrength("1%", { dosageForm: "cream" });
|
|
390
|
+
// → { strengthRatio: { numerator: { value: 1, unit: "g" }, denominator: { value: 100, unit: "g" } } }
|
|
391
|
+
|
|
392
|
+
// Ratios
|
|
393
|
+
parseStrength("250mg/5mL");
|
|
394
|
+
// → { strengthRatio: { numerator: { value: 250, unit: "mg" }, denominator: { value: 5, unit: "mL" } } }
|
|
395
|
+
|
|
396
|
+
// Composite (sums components into a single ratio)
|
|
397
|
+
parseStrength("875mg + 125mg");
|
|
398
|
+
// → { strengthQuantity: { value: 1000, unit: "mg" } }
|
|
399
|
+
|
|
400
|
+
// Simple Quantity
|
|
401
|
+
parseStrength("500mg");
|
|
402
|
+
// → { strengthQuantity: { value: 500, unit: "mg" } }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`parseStrengthIntoRatio` is also available if you specifically need a FHIR Ratio object regardless of the denominator.
|
|
406
|
+
|
|
355
407
|
### Ocular & intravitreal shortcuts
|
|
356
408
|
|
|
357
409
|
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/fhir.js
CHANGED
|
@@ -9,7 +9,7 @@ const object_1 = require("./utils/object");
|
|
|
9
9
|
const array_1 = require("./utils/array");
|
|
10
10
|
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
11
11
|
function toFhir(internal) {
|
|
12
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
12
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
13
13
|
const dosage = {};
|
|
14
14
|
const repeat = {};
|
|
15
15
|
let hasRepeat = false;
|
|
@@ -42,6 +42,10 @@ function toFhir(internal) {
|
|
|
42
42
|
repeat.when = [...internal.when];
|
|
43
43
|
hasRepeat = true;
|
|
44
44
|
}
|
|
45
|
+
if ((_a = internal.timeOfDay) === null || _a === void 0 ? void 0 : _a.length) {
|
|
46
|
+
repeat.timeOfDay = [...internal.timeOfDay];
|
|
47
|
+
hasRepeat = true;
|
|
48
|
+
}
|
|
45
49
|
if (hasRepeat) {
|
|
46
50
|
dosage.timing = { repeat };
|
|
47
51
|
}
|
|
@@ -49,7 +53,7 @@ function toFhir(internal) {
|
|
|
49
53
|
dosage.timing = {};
|
|
50
54
|
}
|
|
51
55
|
if (internal.timingCode) {
|
|
52
|
-
dosage.timing = (
|
|
56
|
+
dosage.timing = (_b = dosage.timing) !== null && _b !== void 0 ? _b : {};
|
|
53
57
|
dosage.timing.code = {
|
|
54
58
|
coding: [{ code: internal.timingCode }],
|
|
55
59
|
text: internal.timingCode
|
|
@@ -82,7 +86,7 @@ function toFhir(internal) {
|
|
|
82
86
|
// Emit SNOMED-coded routes whenever we have parsed or inferred route data.
|
|
83
87
|
if (internal.routeCode || internal.routeText) {
|
|
84
88
|
const coding = internal.routeCode ? maps_1.ROUTE_SNOMED[internal.routeCode] : undefined;
|
|
85
|
-
const text = (
|
|
89
|
+
const text = (_c = internal.routeText) !== null && _c !== void 0 ? _c : (internal.routeCode ? maps_1.ROUTE_TEXT[internal.routeCode] : undefined);
|
|
86
90
|
if (coding) {
|
|
87
91
|
// Provide both text and coding so human-readable and coded systems align.
|
|
88
92
|
dosage.route = {
|
|
@@ -100,11 +104,11 @@ function toFhir(internal) {
|
|
|
100
104
|
dosage.route = { text };
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
|
-
if (internal.siteText || ((
|
|
104
|
-
const coding = ((
|
|
107
|
+
if (internal.siteText || ((_d = internal.siteCoding) === null || _d === void 0 ? void 0 : _d.code)) {
|
|
108
|
+
const coding = ((_e = internal.siteCoding) === null || _e === void 0 ? void 0 : _e.code)
|
|
105
109
|
? [
|
|
106
110
|
{
|
|
107
|
-
system: (
|
|
111
|
+
system: (_f = internal.siteCoding.system) !== null && _f !== void 0 ? _f : SNOMED_SYSTEM,
|
|
108
112
|
code: internal.siteCoding.code,
|
|
109
113
|
display: internal.siteCoding.display
|
|
110
114
|
}
|
|
@@ -115,7 +119,7 @@ function toFhir(internal) {
|
|
|
115
119
|
coding
|
|
116
120
|
};
|
|
117
121
|
}
|
|
118
|
-
if ((
|
|
122
|
+
if ((_g = internal.additionalInstructions) === null || _g === void 0 ? void 0 : _g.length) {
|
|
119
123
|
dosage.additionalInstruction = internal.additionalInstructions.map((instruction) => {
|
|
120
124
|
var _a, _b;
|
|
121
125
|
return ({
|
|
@@ -134,15 +138,15 @@ function toFhir(internal) {
|
|
|
134
138
|
}
|
|
135
139
|
if (internal.asNeeded) {
|
|
136
140
|
dosage.asNeededBoolean = true;
|
|
137
|
-
if (internal.asNeededReason || ((
|
|
141
|
+
if (internal.asNeededReason || ((_h = internal.asNeededReasonCoding) === null || _h === void 0 ? void 0 : _h.code)) {
|
|
138
142
|
const concept = {};
|
|
139
143
|
if (internal.asNeededReason) {
|
|
140
144
|
concept.text = internal.asNeededReason;
|
|
141
145
|
}
|
|
142
|
-
if ((
|
|
146
|
+
if ((_j = internal.asNeededReasonCoding) === null || _j === void 0 ? void 0 : _j.code) {
|
|
143
147
|
concept.coding = [
|
|
144
148
|
{
|
|
145
|
-
system: (
|
|
149
|
+
system: (_k = internal.asNeededReasonCoding.system) !== null && _k !== void 0 ? _k : SNOMED_SYSTEM,
|
|
146
150
|
code: internal.asNeededReasonCoding.code,
|
|
147
151
|
display: internal.asNeededReasonCoding.display
|
|
148
152
|
}
|
|
@@ -158,7 +162,7 @@ function toFhir(internal) {
|
|
|
158
162
|
return dosage;
|
|
159
163
|
}
|
|
160
164
|
function internalFromFhir(dosage) {
|
|
161
|
-
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, _4, _5, _6, _7, _8, _9, _10, _11;
|
|
165
|
+
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, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13;
|
|
162
166
|
const internal = {
|
|
163
167
|
input: (_a = dosage.text) !== null && _a !== void 0 ? _a : "",
|
|
164
168
|
tokens: [],
|
|
@@ -169,24 +173,27 @@ function internalFromFhir(dosage) {
|
|
|
169
173
|
when: ((_e = (_d = dosage.timing) === null || _d === void 0 ? void 0 : _d.repeat) === null || _e === void 0 ? void 0 : _e.when)
|
|
170
174
|
? dosage.timing.repeat.when.filter((value) => (0, array_1.arrayIncludes)((0, object_1.objectValues)(types_1.EventTiming), value))
|
|
171
175
|
: [],
|
|
176
|
+
timeOfDay: ((_g = (_f = dosage.timing) === null || _f === void 0 ? void 0 : _f.repeat) === null || _g === void 0 ? void 0 : _g.timeOfDay)
|
|
177
|
+
? [...dosage.timing.repeat.timeOfDay]
|
|
178
|
+
: [],
|
|
172
179
|
warnings: [],
|
|
173
|
-
timingCode: (
|
|
174
|
-
count: (
|
|
175
|
-
frequency: (
|
|
176
|
-
frequencyMax: (
|
|
177
|
-
period: (
|
|
178
|
-
periodMax: (
|
|
179
|
-
periodUnit: (
|
|
180
|
-
routeText: (
|
|
181
|
-
siteText: (
|
|
180
|
+
timingCode: (_l = (_k = (_j = (_h = dosage.timing) === null || _h === void 0 ? void 0 : _h.code) === null || _j === void 0 ? void 0 : _j.coding) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.code,
|
|
181
|
+
count: (_o = (_m = dosage.timing) === null || _m === void 0 ? void 0 : _m.repeat) === null || _o === void 0 ? void 0 : _o.count,
|
|
182
|
+
frequency: (_q = (_p = dosage.timing) === null || _p === void 0 ? void 0 : _p.repeat) === null || _q === void 0 ? void 0 : _q.frequency,
|
|
183
|
+
frequencyMax: (_s = (_r = dosage.timing) === null || _r === void 0 ? void 0 : _r.repeat) === null || _s === void 0 ? void 0 : _s.frequencyMax,
|
|
184
|
+
period: (_u = (_t = dosage.timing) === null || _t === void 0 ? void 0 : _t.repeat) === null || _u === void 0 ? void 0 : _u.period,
|
|
185
|
+
periodMax: (_w = (_v = dosage.timing) === null || _v === void 0 ? void 0 : _v.repeat) === null || _w === void 0 ? void 0 : _w.periodMax,
|
|
186
|
+
periodUnit: (_y = (_x = dosage.timing) === null || _x === void 0 ? void 0 : _x.repeat) === null || _y === void 0 ? void 0 : _y.periodUnit,
|
|
187
|
+
routeText: (_z = dosage.route) === null || _z === void 0 ? void 0 : _z.text,
|
|
188
|
+
siteText: (_0 = dosage.site) === null || _0 === void 0 ? void 0 : _0.text,
|
|
182
189
|
asNeeded: dosage.asNeededBoolean,
|
|
183
|
-
asNeededReason: (
|
|
190
|
+
asNeededReason: (_2 = (_1 = dosage.asNeededFor) === null || _1 === void 0 ? void 0 : _1[0]) === null || _2 === void 0 ? void 0 : _2.text,
|
|
184
191
|
siteTokenIndices: new Set(),
|
|
185
192
|
siteLookups: [],
|
|
186
193
|
prnReasonLookups: [],
|
|
187
194
|
additionalInstructions: []
|
|
188
195
|
};
|
|
189
|
-
const routeCoding = (
|
|
196
|
+
const routeCoding = (_4 = (_3 = dosage.route) === null || _3 === void 0 ? void 0 : _3.coding) === null || _4 === void 0 ? void 0 : _4.find((code) => code.system === SNOMED_SYSTEM);
|
|
190
197
|
if (routeCoding === null || routeCoding === void 0 ? void 0 : routeCoding.code) {
|
|
191
198
|
// Translate SNOMED codings back into the simplified enum for round-trip fidelity.
|
|
192
199
|
const mapped = maps_1.ROUTE_BY_SNOMED[routeCoding.code];
|
|
@@ -195,7 +202,7 @@ function internalFromFhir(dosage) {
|
|
|
195
202
|
internal.routeText = maps_1.ROUTE_TEXT[mapped];
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
|
-
const siteCoding = (
|
|
205
|
+
const siteCoding = (_6 = (_5 = dosage.site) === null || _5 === void 0 ? void 0 : _5.coding) === null || _6 === void 0 ? void 0 : _6.find((code) => code.system === SNOMED_SYSTEM);
|
|
199
206
|
if (siteCoding === null || siteCoding === void 0 ? void 0 : siteCoding.code) {
|
|
200
207
|
internal.siteCoding = {
|
|
201
208
|
code: siteCoding.code,
|
|
@@ -203,7 +210,7 @@ function internalFromFhir(dosage) {
|
|
|
203
210
|
system: siteCoding.system
|
|
204
211
|
};
|
|
205
212
|
}
|
|
206
|
-
const reasonCoding = (
|
|
213
|
+
const reasonCoding = (_9 = (_8 = (_7 = dosage.asNeededFor) === null || _7 === void 0 ? void 0 : _7[0]) === null || _8 === void 0 ? void 0 : _8.coding) === null || _9 === void 0 ? void 0 : _9[0];
|
|
207
214
|
if (reasonCoding === null || reasonCoding === void 0 ? void 0 : reasonCoding.code) {
|
|
208
215
|
internal.asNeededReasonCoding = {
|
|
209
216
|
code: reasonCoding.code,
|
|
@@ -211,7 +218,7 @@ function internalFromFhir(dosage) {
|
|
|
211
218
|
system: reasonCoding.system
|
|
212
219
|
};
|
|
213
220
|
}
|
|
214
|
-
if ((
|
|
221
|
+
if ((_10 = dosage.additionalInstruction) === null || _10 === void 0 ? void 0 : _10.length) {
|
|
215
222
|
internal.additionalInstructions = dosage.additionalInstruction.map((concept) => {
|
|
216
223
|
var _a;
|
|
217
224
|
return ({
|
|
@@ -226,13 +233,13 @@ function internalFromFhir(dosage) {
|
|
|
226
233
|
});
|
|
227
234
|
});
|
|
228
235
|
}
|
|
229
|
-
const doseAndRate = (
|
|
236
|
+
const doseAndRate = (_11 = dosage.doseAndRate) === null || _11 === void 0 ? void 0 : _11[0];
|
|
230
237
|
if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseRange) {
|
|
231
238
|
const { low, high } = doseAndRate.doseRange;
|
|
232
239
|
if ((low === null || low === void 0 ? void 0 : low.value) !== undefined && (high === null || high === void 0 ? void 0 : high.value) !== undefined) {
|
|
233
240
|
internal.doseRange = { low: low.value, high: high.value };
|
|
234
241
|
}
|
|
235
|
-
internal.unit = (
|
|
242
|
+
internal.unit = (_13 = (_12 = low === null || low === void 0 ? void 0 : low.unit) !== null && _12 !== void 0 ? _12 : high === null || high === void 0 ? void 0 : high.unit) !== null && _13 !== void 0 ? _13 : internal.unit;
|
|
236
243
|
}
|
|
237
244
|
else if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseQuantity) {
|
|
238
245
|
const dose = doseAndRate.doseQuantity;
|
package/dist/format.js
CHANGED
|
@@ -515,6 +515,7 @@ function formatInternal(internal, style, localization) {
|
|
|
515
515
|
return defaults[style];
|
|
516
516
|
}
|
|
517
517
|
function formatShort(internal) {
|
|
518
|
+
var _a;
|
|
518
519
|
const parts = [];
|
|
519
520
|
const dosePart = formatDoseShort(internal);
|
|
520
521
|
if (dosePart) {
|
|
@@ -561,6 +562,9 @@ function formatShort(internal) {
|
|
|
561
562
|
.map((d) => d.charAt(0).toUpperCase() + d.slice(1, 3))
|
|
562
563
|
.join(","));
|
|
563
564
|
}
|
|
565
|
+
if ((_a = internal.timeOfDay) === null || _a === void 0 ? void 0 : _a.length) {
|
|
566
|
+
parts.push(internal.timeOfDay.map(t => t.slice(0, 5)).join(","));
|
|
567
|
+
}
|
|
564
568
|
if (internal.count !== undefined) {
|
|
565
569
|
parts.push(`x${stripTrailingZero(internal.count)}`);
|
|
566
570
|
}
|
|
@@ -575,13 +579,24 @@ function formatShort(internal) {
|
|
|
575
579
|
return parts.filter(Boolean).join(" ");
|
|
576
580
|
}
|
|
577
581
|
function formatLong(internal) {
|
|
578
|
-
var _a;
|
|
582
|
+
var _a, _b;
|
|
579
583
|
const grammar = resolveRouteGrammar(internal);
|
|
580
584
|
const dosePart = (_a = formatDoseLong(internal)) !== null && _a !== void 0 ? _a : "the medication";
|
|
581
585
|
const sitePart = formatSite(internal, grammar);
|
|
582
586
|
const routePart = buildRoutePhrase(internal, grammar, Boolean(sitePart));
|
|
583
587
|
const frequencyPart = describeFrequency(internal);
|
|
584
588
|
const eventParts = collectWhenPhrases(internal);
|
|
589
|
+
if ((_b = internal.timeOfDay) === null || _b === void 0 ? void 0 : _b.length) {
|
|
590
|
+
for (const time of internal.timeOfDay) {
|
|
591
|
+
const parts = time.split(":");
|
|
592
|
+
const h = parseInt(parts[0], 10);
|
|
593
|
+
const m = parseInt(parts[1], 10);
|
|
594
|
+
const isAm = h < 12;
|
|
595
|
+
const displayH = h % 12 || 12;
|
|
596
|
+
const displayM = m < 10 ? `0${m}` : `${m}`;
|
|
597
|
+
eventParts.push(`at ${displayH}:${displayM}${isAm ? " am" : " pm"}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
585
600
|
const timing = combineFrequencyAndEvents(frequencyPart, eventParts);
|
|
586
601
|
const dayPart = describeDayOfWeek(internal);
|
|
587
602
|
const countPart = internal.count
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { FhirDosage, FormatOptions, ParseOptions, ParseResult } from "./types";
|
|
1
|
+
import { FhirDosage, FormatOptions, LintResult, ParseOptions, ParseResult } from "./types";
|
|
2
2
|
export { parseInternal } from "./parser";
|
|
3
3
|
export { suggestSig } from "./suggest";
|
|
4
4
|
export * from "./types";
|
|
5
|
-
export { nextDueDoses } from "./schedule";
|
|
5
|
+
export { nextDueDoses, calculateTotalUnits } from "./schedule";
|
|
6
|
+
export { parseStrength, parseStrengthIntoRatio } from "./utils/strength";
|
|
6
7
|
export { getRegisteredSigLocalizations, registerSigLocalization, resolveSigLocalization, resolveSigTranslation } from "./i18n";
|
|
7
8
|
export type { SigLocalization, SigLocalizationConfig, SigTranslation, SigTranslationConfig } from "./i18n";
|
|
8
9
|
export declare function parseSig(input: string, options?: ParseOptions): ParseResult;
|
|
10
|
+
export declare function lintSig(input: string, options?: ParseOptions): LintResult;
|
|
9
11
|
export declare function parseSigAsync(input: string, options?: ParseOptions): Promise<ParseResult>;
|
|
10
12
|
export declare function formatSig(dosage: FhirDosage, style?: "short" | "long", options?: FormatOptions): string;
|
|
11
13
|
export declare function fromFhirDosage(dosage: FhirDosage, options?: FormatOptions): ParseResult;
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.resolveSigTranslation = exports.resolveSigLocalization = exports.registerSigLocalization = exports.getRegisteredSigLocalizations = exports.nextDueDoses = exports.suggestSig = exports.parseInternal = void 0;
|
|
26
|
+
exports.resolveSigTranslation = exports.resolveSigLocalization = exports.registerSigLocalization = exports.getRegisteredSigLocalizations = exports.parseStrengthIntoRatio = exports.parseStrength = exports.calculateTotalUnits = exports.nextDueDoses = exports.suggestSig = exports.parseInternal = void 0;
|
|
27
27
|
exports.parseSig = parseSig;
|
|
28
|
+
exports.lintSig = lintSig;
|
|
28
29
|
exports.parseSigAsync = parseSigAsync;
|
|
29
30
|
exports.formatSig = formatSig;
|
|
30
31
|
exports.fromFhirDosage = fromFhirDosage;
|
|
@@ -39,6 +40,10 @@ Object.defineProperty(exports, "suggestSig", { enumerable: true, get: function (
|
|
|
39
40
|
__exportStar(require("./types"), exports);
|
|
40
41
|
var schedule_1 = require("./schedule");
|
|
41
42
|
Object.defineProperty(exports, "nextDueDoses", { enumerable: true, get: function () { return schedule_1.nextDueDoses; } });
|
|
43
|
+
Object.defineProperty(exports, "calculateTotalUnits", { enumerable: true, get: function () { return schedule_1.calculateTotalUnits; } });
|
|
44
|
+
var strength_1 = require("./utils/strength");
|
|
45
|
+
Object.defineProperty(exports, "parseStrength", { enumerable: true, get: function () { return strength_1.parseStrength; } });
|
|
46
|
+
Object.defineProperty(exports, "parseStrengthIntoRatio", { enumerable: true, get: function () { return strength_1.parseStrengthIntoRatio; } });
|
|
42
47
|
var i18n_2 = require("./i18n");
|
|
43
48
|
Object.defineProperty(exports, "getRegisteredSigLocalizations", { enumerable: true, get: function () { return i18n_2.getRegisteredSigLocalizations; } });
|
|
44
49
|
Object.defineProperty(exports, "registerSigLocalization", { enumerable: true, get: function () { return i18n_2.registerSigLocalization; } });
|
|
@@ -50,6 +55,25 @@ function parseSig(input, options) {
|
|
|
50
55
|
(0, parser_1.applySiteCoding)(internal, options);
|
|
51
56
|
return buildParseResult(internal, options);
|
|
52
57
|
}
|
|
58
|
+
function lintSig(input, options) {
|
|
59
|
+
const internal = (0, parser_1.parseInternal)(input, options);
|
|
60
|
+
(0, parser_1.applyPrnReasonCoding)(internal, options);
|
|
61
|
+
(0, parser_1.applySiteCoding)(internal, options);
|
|
62
|
+
const result = buildParseResult(internal, options);
|
|
63
|
+
const groups = (0, parser_1.findUnparsedTokenGroups)(internal);
|
|
64
|
+
const issues = groups.map((group) => {
|
|
65
|
+
const text = group.range
|
|
66
|
+
? internal.input.slice(group.range.start, group.range.end)
|
|
67
|
+
: group.tokens.map((token) => token.original).join(" ");
|
|
68
|
+
return {
|
|
69
|
+
message: "Unrecognized text",
|
|
70
|
+
text: text.trim() || text,
|
|
71
|
+
tokens: group.tokens.map((token) => token.original),
|
|
72
|
+
range: group.range
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
return { result, issues };
|
|
76
|
+
}
|
|
53
77
|
function parseSigAsync(input, options) {
|
|
54
78
|
return __awaiter(this, void 0, void 0, function* () {
|
|
55
79
|
const internal = (0, parser_1.parseInternal)(input, options);
|
package/dist/internal-types.d.ts
CHANGED
package/dist/parser.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ParsedSigInternal, Token } from "./internal-types";
|
|
2
|
-
import { ParseOptions } from "./types";
|
|
2
|
+
import { ParseOptions, TextRange } from "./types";
|
|
3
3
|
export declare function tokenize(input: string): Token[];
|
|
4
|
+
export declare function findUnparsedTokenGroups(internal: ParsedSigInternal): Array<{
|
|
5
|
+
tokens: Token[];
|
|
6
|
+
range?: TextRange;
|
|
7
|
+
}>;
|
|
4
8
|
export declare function parseInternal(input: string, options?: ParseOptions): ParsedSigInternal;
|
|
5
9
|
/**
|
|
6
10
|
* Resolves parsed site text against SNOMED dictionaries and synchronous
|
package/dist/parser.js
CHANGED
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.tokenize = tokenize;
|
|
13
|
+
exports.findUnparsedTokenGroups = findUnparsedTokenGroups;
|
|
13
14
|
exports.parseInternal = parseInternal;
|
|
14
15
|
exports.applyPrnReasonCoding = applyPrnReasonCoding;
|
|
15
16
|
exports.applyPrnReasonCodingAsync = applyPrnReasonCodingAsync;
|
|
@@ -748,6 +749,114 @@ function tryParseCountBasedFrequency(internal, tokens, index, options) {
|
|
|
748
749
|
}
|
|
749
750
|
return consumeCurrentToken;
|
|
750
751
|
}
|
|
752
|
+
function parseTimeToFhir(timeStr) {
|
|
753
|
+
const clean = timeStr.toLowerCase().trim();
|
|
754
|
+
// Match 9:00, 9.00, 9:00am, 9pm, 9 am, 9
|
|
755
|
+
const match = clean.match(/^(\d{1,2})[:.](\d{2})\s*(am|pm)?$/) ||
|
|
756
|
+
clean.match(/^(\d{1,2})\s*(am|pm)$/) ||
|
|
757
|
+
clean.match(/^(\d{1,2})$/);
|
|
758
|
+
if (!match)
|
|
759
|
+
return undefined;
|
|
760
|
+
let hour = parseInt(match[1], 10);
|
|
761
|
+
let minute = 0;
|
|
762
|
+
let ampm;
|
|
763
|
+
if (match[2] && !isNaN(parseInt(match[2], 10))) {
|
|
764
|
+
minute = parseInt(match[2], 10);
|
|
765
|
+
ampm = match[3];
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
ampm = match[2];
|
|
769
|
+
}
|
|
770
|
+
if (ampm === "pm" && hour < 12)
|
|
771
|
+
hour += 12;
|
|
772
|
+
if (ampm === "am" && hour === 12)
|
|
773
|
+
hour = 0;
|
|
774
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
775
|
+
return undefined;
|
|
776
|
+
const h = hour < 10 ? `0${hour}` : `${hour}`;
|
|
777
|
+
const m = minute < 10 ? `0${minute}` : `${minute}`;
|
|
778
|
+
return `${h}:${m}:00`;
|
|
779
|
+
}
|
|
780
|
+
function tryParseTimeBasedSchedule(internal, tokens, index) {
|
|
781
|
+
const token = tokens[index];
|
|
782
|
+
if (internal.consumed.has(token.index))
|
|
783
|
+
return false;
|
|
784
|
+
const isAtPrefix = token.lower === "@" || token.lower === "at";
|
|
785
|
+
if (!isAtPrefix && !/^\d/.test(token.lower))
|
|
786
|
+
return false;
|
|
787
|
+
let nextIndex = isAtPrefix ? index + 1 : index;
|
|
788
|
+
const times = [];
|
|
789
|
+
const consumedIndices = [];
|
|
790
|
+
const timeTokens = [];
|
|
791
|
+
if (isAtPrefix)
|
|
792
|
+
consumedIndices.push(index);
|
|
793
|
+
while (nextIndex < tokens.length) {
|
|
794
|
+
const nextToken = tokens[nextIndex];
|
|
795
|
+
if (!nextToken || internal.consumed.has(nextToken.index))
|
|
796
|
+
break;
|
|
797
|
+
let timeStr = nextToken.lower;
|
|
798
|
+
let lookaheadIndices = [];
|
|
799
|
+
// Look ahead for am/pm if current token is just a number or doesn't have am/pm
|
|
800
|
+
if (!timeStr.includes("am") && !timeStr.includes("pm")) {
|
|
801
|
+
const nextNext = tokens[nextIndex + 1];
|
|
802
|
+
if (nextNext && !internal.consumed.has(nextNext.index) && (nextNext.lower === "am" || nextNext.lower === "pm")) {
|
|
803
|
+
timeStr += nextNext.lower;
|
|
804
|
+
lookaheadIndices.push(nextIndex + 1);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const time = parseTimeToFhir(timeStr);
|
|
808
|
+
if (time) {
|
|
809
|
+
times.push(time);
|
|
810
|
+
timeTokens.push(timeStr);
|
|
811
|
+
consumedIndices.push(nextIndex);
|
|
812
|
+
for (const idx of lookaheadIndices) {
|
|
813
|
+
consumedIndices.push(idx);
|
|
814
|
+
}
|
|
815
|
+
nextIndex += 1 + lookaheadIndices.length;
|
|
816
|
+
// Support comma or space separated times
|
|
817
|
+
const separatorToken = tokens[nextIndex];
|
|
818
|
+
// Check if there is another time after the separator
|
|
819
|
+
if (separatorToken && (separatorToken.lower === "," || separatorToken.lower === "and")) {
|
|
820
|
+
// Peek for next time
|
|
821
|
+
let peekIndex = nextIndex + 1;
|
|
822
|
+
let peekToken = tokens[peekIndex];
|
|
823
|
+
if (peekToken) {
|
|
824
|
+
let peekStr = peekToken.lower;
|
|
825
|
+
let peekNext = tokens[peekIndex + 1];
|
|
826
|
+
if (peekNext && !internal.consumed.has(peekNext.index) && (peekNext.lower === "am" || peekNext.lower === "pm")) {
|
|
827
|
+
peekStr += peekNext.lower;
|
|
828
|
+
}
|
|
829
|
+
if (parseTimeToFhir(peekStr)) {
|
|
830
|
+
consumedIndices.push(nextIndex);
|
|
831
|
+
nextIndex++;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
if (times.length > 0) {
|
|
841
|
+
if (!isAtPrefix) {
|
|
842
|
+
const hasClearTimeFormat = timeTokens.some((t) => t.includes(":") || t.includes("am") || t.includes("pm"));
|
|
843
|
+
if (!hasClearTimeFormat) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
internal.timeOfDay = internal.timeOfDay || [];
|
|
848
|
+
for (const time of times) {
|
|
849
|
+
if (!(0, array_1.arrayIncludes)(internal.timeOfDay, time)) {
|
|
850
|
+
internal.timeOfDay.push(time);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const idx of consumedIndices) {
|
|
854
|
+
mark(internal.consumed, tokens[idx]);
|
|
855
|
+
}
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
751
860
|
const SITE_UNIT_ROUTE_HINTS = [
|
|
752
861
|
{ pattern: /\beye(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
|
|
753
862
|
{ pattern: /\beyelid(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
|
|
@@ -885,6 +994,68 @@ function refineSiteRange(input, sanitized, tokenRange) {
|
|
|
885
994
|
}
|
|
886
995
|
return { start: startIndex, end: startIndex + lowerSanitized.length };
|
|
887
996
|
}
|
|
997
|
+
function findUnparsedTokenGroups(internal) {
|
|
998
|
+
const leftoverTokens = internal.tokens
|
|
999
|
+
.filter((token) => !internal.consumed.has(token.index))
|
|
1000
|
+
.sort((a, b) => a.index - b.index);
|
|
1001
|
+
if (leftoverTokens.length === 0) {
|
|
1002
|
+
return [];
|
|
1003
|
+
}
|
|
1004
|
+
const groups = [];
|
|
1005
|
+
let currentGroup = [];
|
|
1006
|
+
let previousIndex;
|
|
1007
|
+
let minimumStart = 0;
|
|
1008
|
+
const locateRange = (tokensToLocate, initial) => {
|
|
1009
|
+
const lowerInput = internal.input.toLowerCase();
|
|
1010
|
+
let searchStart = minimumStart;
|
|
1011
|
+
let rangeStart;
|
|
1012
|
+
let rangeEnd;
|
|
1013
|
+
for (const token of tokensToLocate) {
|
|
1014
|
+
const segment = token.original.trim();
|
|
1015
|
+
if (!segment) {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
const lowerSegment = segment.toLowerCase();
|
|
1019
|
+
const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
|
|
1020
|
+
if (foundIndex === -1) {
|
|
1021
|
+
return initial;
|
|
1022
|
+
}
|
|
1023
|
+
if (rangeStart === undefined) {
|
|
1024
|
+
rangeStart = foundIndex;
|
|
1025
|
+
}
|
|
1026
|
+
const segmentEnd = foundIndex + lowerSegment.length;
|
|
1027
|
+
rangeEnd = rangeEnd === undefined ? segmentEnd : Math.max(rangeEnd, segmentEnd);
|
|
1028
|
+
searchStart = segmentEnd;
|
|
1029
|
+
}
|
|
1030
|
+
if (rangeStart === undefined || rangeEnd === undefined) {
|
|
1031
|
+
return initial;
|
|
1032
|
+
}
|
|
1033
|
+
return { start: rangeStart, end: rangeEnd };
|
|
1034
|
+
};
|
|
1035
|
+
const flush = () => {
|
|
1036
|
+
if (!currentGroup.length) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const indices = currentGroup.map((token) => token.index);
|
|
1040
|
+
const initialRange = computeTokenRange(internal.input, internal.tokens, indices);
|
|
1041
|
+
const range = locateRange(currentGroup, initialRange);
|
|
1042
|
+
groups.push({ tokens: currentGroup, range });
|
|
1043
|
+
if (range) {
|
|
1044
|
+
minimumStart = Math.max(minimumStart, range.end);
|
|
1045
|
+
}
|
|
1046
|
+
currentGroup = [];
|
|
1047
|
+
previousIndex = undefined;
|
|
1048
|
+
};
|
|
1049
|
+
for (const token of leftoverTokens) {
|
|
1050
|
+
if (previousIndex !== undefined && token.index !== previousIndex + 1) {
|
|
1051
|
+
flush();
|
|
1052
|
+
}
|
|
1053
|
+
currentGroup.push(token);
|
|
1054
|
+
previousIndex = token.index;
|
|
1055
|
+
}
|
|
1056
|
+
flush();
|
|
1057
|
+
return groups;
|
|
1058
|
+
}
|
|
888
1059
|
function splitToken(token) {
|
|
889
1060
|
if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
|
|
890
1061
|
return [token];
|
|
@@ -1322,7 +1493,8 @@ function applyWhenToken(internal, token, code) {
|
|
|
1322
1493
|
addWhen(internal.when, code);
|
|
1323
1494
|
mark(internal.consumed, token);
|
|
1324
1495
|
}
|
|
1325
|
-
function
|
|
1496
|
+
function parseAnchorSequence(internal, tokens, index, prefixCode) {
|
|
1497
|
+
var _a;
|
|
1326
1498
|
const token = tokens[index];
|
|
1327
1499
|
let converted = 0;
|
|
1328
1500
|
for (let lookahead = index + 1; lookahead < tokens.length; lookahead++) {
|
|
@@ -1330,30 +1502,57 @@ function parseMealContext(internal, tokens, index, code) {
|
|
|
1330
1502
|
if (internal.consumed.has(nextToken.index)) {
|
|
1331
1503
|
continue;
|
|
1332
1504
|
}
|
|
1333
|
-
|
|
1505
|
+
const lower = nextToken.lower;
|
|
1506
|
+
if (MEAL_CONTEXT_CONNECTORS.has(lower) || lower === ",") {
|
|
1334
1507
|
mark(internal.consumed, nextToken);
|
|
1335
1508
|
continue;
|
|
1336
1509
|
}
|
|
1337
|
-
const
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1510
|
+
const day = maps_1.DAY_OF_WEEK_TOKENS[lower];
|
|
1511
|
+
if (day) {
|
|
1512
|
+
if (!(0, array_1.arrayIncludes)(internal.dayOfWeek, day)) {
|
|
1513
|
+
internal.dayOfWeek.push(day);
|
|
1514
|
+
}
|
|
1515
|
+
mark(internal.consumed, nextToken);
|
|
1516
|
+
converted++;
|
|
1517
|
+
continue;
|
|
1340
1518
|
}
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
? meal.
|
|
1345
|
-
:
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1519
|
+
const meal = maps_1.MEAL_KEYWORDS[lower];
|
|
1520
|
+
if (meal) {
|
|
1521
|
+
const whenCode = prefixCode === types_1.EventTiming["After Meal"]
|
|
1522
|
+
? meal.pc
|
|
1523
|
+
: prefixCode === types_1.EventTiming["Before Meal"]
|
|
1524
|
+
? meal.ac
|
|
1525
|
+
: ((_a = maps_1.EVENT_TIMING_TOKENS[lower]) !== null && _a !== void 0 ? _a : meal.pc); // fallback to general or conservative default
|
|
1526
|
+
addWhen(internal.when, whenCode);
|
|
1527
|
+
mark(internal.consumed, nextToken);
|
|
1528
|
+
converted++;
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
const whenCode = maps_1.EVENT_TIMING_TOKENS[lower];
|
|
1532
|
+
if (whenCode) {
|
|
1533
|
+
if (prefixCode && !meal) {
|
|
1534
|
+
// if we have pc/ac, we only want to follow it with explicit meals
|
|
1535
|
+
// to avoid over-consuming anchors that should be separate (like 'pc hs')
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
addWhen(internal.when, whenCode);
|
|
1539
|
+
mark(internal.consumed, nextToken);
|
|
1540
|
+
converted++;
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
break;
|
|
1349
1544
|
}
|
|
1350
1545
|
if (converted > 0) {
|
|
1351
1546
|
mark(internal.consumed, token);
|
|
1352
|
-
return;
|
|
1547
|
+
return true;
|
|
1353
1548
|
}
|
|
1354
|
-
|
|
1549
|
+
if (prefixCode) {
|
|
1550
|
+
applyWhenToken(internal, token, prefixCode);
|
|
1551
|
+
return true;
|
|
1552
|
+
}
|
|
1553
|
+
return false;
|
|
1355
1554
|
}
|
|
1356
|
-
function
|
|
1555
|
+
function parseSeparatedInterval(internal, tokens, index, options) {
|
|
1357
1556
|
const token = tokens[index];
|
|
1358
1557
|
const next = tokens[index + 1];
|
|
1359
1558
|
if (!next || internal.consumed.has(next.index)) {
|
|
@@ -1673,11 +1872,14 @@ function parseInternal(input, options) {
|
|
|
1673
1872
|
applyWhenToken(internal, token, types_1.EventTiming.Meal);
|
|
1674
1873
|
continue;
|
|
1675
1874
|
}
|
|
1676
|
-
if (token.lower === "q") {
|
|
1677
|
-
if (
|
|
1875
|
+
if (token.lower === "q" || token.lower === "every" || token.lower === "each") {
|
|
1876
|
+
if (parseSeparatedInterval(internal, tokens, i, options)) {
|
|
1678
1877
|
continue;
|
|
1679
1878
|
}
|
|
1680
1879
|
}
|
|
1880
|
+
if (tryParseTimeBasedSchedule(internal, tokens, i)) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1681
1883
|
if (tryParseNumericCadence(internal, tokens, i)) {
|
|
1682
1884
|
continue;
|
|
1683
1885
|
}
|
|
@@ -1705,12 +1907,22 @@ function parseInternal(input, options) {
|
|
|
1705
1907
|
continue;
|
|
1706
1908
|
}
|
|
1707
1909
|
// Event timing tokens
|
|
1708
|
-
if (token.lower === "pc" || token.lower === "ac") {
|
|
1709
|
-
|
|
1910
|
+
if (token.lower === "pc" || token.lower === "ac" || token.lower === "after" || token.lower === "before") {
|
|
1911
|
+
parseAnchorSequence(internal, tokens, i, (token.lower === "pc" || token.lower === "after")
|
|
1710
1912
|
? types_1.EventTiming["After Meal"]
|
|
1711
1913
|
: types_1.EventTiming["Before Meal"]);
|
|
1712
1914
|
continue;
|
|
1713
1915
|
}
|
|
1916
|
+
if (token.lower === "at" || token.lower === "@" || token.lower === "on") {
|
|
1917
|
+
if (parseAnchorSequence(internal, tokens, i)) {
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (tryParseTimeBasedSchedule(internal, tokens, i)) {
|
|
1921
|
+
continue;
|
|
1922
|
+
}
|
|
1923
|
+
mark(internal.consumed, token);
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1714
1926
|
const nextToken = tokens[i + 1];
|
|
1715
1927
|
if (nextToken && !internal.consumed.has(nextToken.index)) {
|
|
1716
1928
|
const combo = `${token.lower} ${nextToken.lower}`;
|
package/dist/schedule.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FhirDosage, NextDueDoseOptions } from "./types";
|
|
1
|
+
import { FhirDosage, NextDueDoseOptions, TotalUnitsOptions, TotalUnitsResult } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Produces the next dose timestamps in ascending order according to the
|
|
4
4
|
* provided configuration and dosage metadata.
|
|
5
5
|
*/
|
|
6
6
|
export declare function nextDueDoses(dosage: FhirDosage, options: NextDueDoseOptions): string[];
|
|
7
|
+
export declare function calculateTotalUnits(options: TotalUnitsOptions): TotalUnitsResult;
|
package/dist/schedule.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.nextDueDoses = nextDueDoses;
|
|
4
|
+
exports.calculateTotalUnits = calculateTotalUnits;
|
|
4
5
|
const types_1 = require("./types");
|
|
5
6
|
const array_1 = require("./utils/array");
|
|
7
|
+
const units_1 = require("./utils/units");
|
|
8
|
+
const strength_1 = require("./utils/strength");
|
|
6
9
|
/**
|
|
7
10
|
* Default institution times used when a dosage only specifies frequency without
|
|
8
11
|
* explicit EventTiming anchors. Clinics can override these through the
|
|
@@ -228,8 +231,12 @@ function makeZonedDate(timeZone, year, month, day, hour, minute, second) {
|
|
|
228
231
|
}
|
|
229
232
|
/** Convenience wrapper around makeZonedDate for day-level math. */
|
|
230
233
|
function makeZonedDateFromDay(base, timeZone, clock) {
|
|
234
|
+
var _a, _b, _c;
|
|
231
235
|
const { year, month, day } = getTimeParts(base, timeZone);
|
|
232
|
-
const
|
|
236
|
+
const parts = clock.split(":").map((value) => Number(value));
|
|
237
|
+
const hour = (_a = parts[0]) !== null && _a !== void 0 ? _a : 0;
|
|
238
|
+
const minute = (_b = parts[1]) !== null && _b !== void 0 ? _b : 0;
|
|
239
|
+
const second = (_c = parts[2]) !== null && _c !== void 0 ? _c : 0;
|
|
233
240
|
return makeZonedDate(timeZone, year, month, day, hour, minute, second);
|
|
234
241
|
}
|
|
235
242
|
/** Returns a Date pinned to the start of the local day. */
|
|
@@ -244,7 +251,8 @@ function startOfLocalDay(date, timeZone) {
|
|
|
244
251
|
/** Adds a number of calendar days while remaining aligned to the time zone. */
|
|
245
252
|
function addLocalDays(date, days, timeZone) {
|
|
246
253
|
const { year, month, day } = getTimeParts(date, timeZone);
|
|
247
|
-
const
|
|
254
|
+
const rollover = new Date(Date.UTC(year, month - 1, day + days));
|
|
255
|
+
const zoned = makeZonedDate(timeZone, rollover.getUTCFullYear(), rollover.getUTCMonth() + 1, rollover.getUTCDate(), 0, 0, 0);
|
|
248
256
|
if (!zoned) {
|
|
249
257
|
throw new Error("Unable to shift local day – invalid calendar combination");
|
|
250
258
|
}
|
|
@@ -895,3 +903,202 @@ function addCalendarMonths(date, months, timeZone) {
|
|
|
895
903
|
}
|
|
896
904
|
return final;
|
|
897
905
|
}
|
|
906
|
+
/**
|
|
907
|
+
* Internal helper to count dose events within a time range.
|
|
908
|
+
*/
|
|
909
|
+
function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limit) {
|
|
910
|
+
var _a, _b, _c;
|
|
911
|
+
const timing = dosage.timing;
|
|
912
|
+
const repeat = timing === null || timing === void 0 ? void 0 : timing.repeat;
|
|
913
|
+
if (!timing || !repeat)
|
|
914
|
+
return 0;
|
|
915
|
+
const dayFilter = new Set(((_a = repeat.dayOfWeek) !== null && _a !== void 0 ? _a : []).map((day) => day.toLowerCase()));
|
|
916
|
+
const enforceDayFilter = dayFilter.size > 0;
|
|
917
|
+
const seen = new Set();
|
|
918
|
+
let count = 0;
|
|
919
|
+
const timeZone = config.timeZone;
|
|
920
|
+
const recordCandidate = (candidate) => {
|
|
921
|
+
if (!candidate)
|
|
922
|
+
return false;
|
|
923
|
+
if (candidate < from || candidate >= to)
|
|
924
|
+
return false;
|
|
925
|
+
const iso = formatZonedIso(candidate, timeZone);
|
|
926
|
+
if (seen.has(iso))
|
|
927
|
+
return false;
|
|
928
|
+
seen.add(iso);
|
|
929
|
+
count += 1;
|
|
930
|
+
return true;
|
|
931
|
+
};
|
|
932
|
+
const whenCodes = (_b = repeat.when) !== null && _b !== void 0 ? _b : [];
|
|
933
|
+
const timeOfDayEntries = (_c = repeat.timeOfDay) !== null && _c !== void 0 ? _c : [];
|
|
934
|
+
if (whenCodes.length > 0 || timeOfDayEntries.length > 0) {
|
|
935
|
+
const expanded = expandWhenCodes(whenCodes, config, repeat);
|
|
936
|
+
if (timeOfDayEntries.length > 0) {
|
|
937
|
+
for (const clock of timeOfDayEntries) {
|
|
938
|
+
expanded.push({ time: normalizeClock(clock), dayShift: 0 });
|
|
939
|
+
}
|
|
940
|
+
expanded.sort((a, b) => {
|
|
941
|
+
if (a.dayShift !== b.dayShift)
|
|
942
|
+
return a.dayShift - b.dayShift;
|
|
943
|
+
return a.time.localeCompare(b.time);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if ((0, array_1.arrayIncludes)(whenCodes, types_1.EventTiming.Immediate)) {
|
|
947
|
+
const immediateSource = orderedAt !== null && orderedAt !== void 0 ? orderedAt : from;
|
|
948
|
+
if (!orderedAt || orderedAt >= from) {
|
|
949
|
+
recordCandidate(immediateSource);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (expanded.length === 0)
|
|
953
|
+
return count;
|
|
954
|
+
let currentDay = startOfLocalDay(from, timeZone);
|
|
955
|
+
let iterations = 0;
|
|
956
|
+
const maxIterations = limit !== undefined ? limit * 31 : 365 * 31;
|
|
957
|
+
while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDay < to && iterations < maxIterations) {
|
|
958
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
959
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
960
|
+
for (const entry of expanded) {
|
|
961
|
+
const targetDay = entry.dayShift === 0
|
|
962
|
+
? currentDay
|
|
963
|
+
: addLocalDays(currentDay, entry.dayShift, timeZone);
|
|
964
|
+
const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
|
|
965
|
+
if (zoned)
|
|
966
|
+
recordCandidate(zoned);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
970
|
+
iterations += 1;
|
|
971
|
+
}
|
|
972
|
+
return count;
|
|
973
|
+
}
|
|
974
|
+
const treatAsInterval = !!repeat.period &&
|
|
975
|
+
!!repeat.periodUnit &&
|
|
976
|
+
(!repeat.frequency ||
|
|
977
|
+
repeat.periodUnit !== "d" ||
|
|
978
|
+
(repeat.frequency === 1 && repeat.period > 1)) &&
|
|
979
|
+
!enforceDayFilter;
|
|
980
|
+
if (treatAsInterval) {
|
|
981
|
+
const increment = createIntervalStepper(repeat, timeZone);
|
|
982
|
+
if (!increment)
|
|
983
|
+
return count;
|
|
984
|
+
let current = baseTime;
|
|
985
|
+
let guard = 0;
|
|
986
|
+
const maxIterations = limit !== undefined ? limit * 1000 : 10000;
|
|
987
|
+
// Advance to "from"
|
|
988
|
+
while (current < from && guard < maxIterations) {
|
|
989
|
+
const next = increment(current);
|
|
990
|
+
if (!next || next.getTime() === current.getTime())
|
|
991
|
+
break;
|
|
992
|
+
current = next;
|
|
993
|
+
guard++;
|
|
994
|
+
}
|
|
995
|
+
while (current < to && count < (limit !== null && limit !== void 0 ? limit : Infinity) && guard < maxIterations) {
|
|
996
|
+
const weekday = getLocalWeekday(current, timeZone);
|
|
997
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
998
|
+
recordCandidate(current);
|
|
999
|
+
}
|
|
1000
|
+
const next = increment(current);
|
|
1001
|
+
if (!next || next.getTime() === current.getTime())
|
|
1002
|
+
break;
|
|
1003
|
+
current = next;
|
|
1004
|
+
guard += 1;
|
|
1005
|
+
}
|
|
1006
|
+
return count;
|
|
1007
|
+
}
|
|
1008
|
+
if (repeat.frequency && repeat.period && repeat.periodUnit) {
|
|
1009
|
+
const clocks = resolveFrequencyClocks(timing, config);
|
|
1010
|
+
if (clocks.length === 0)
|
|
1011
|
+
return count;
|
|
1012
|
+
let currentDay = startOfLocalDay(from, timeZone);
|
|
1013
|
+
let iterations = 0;
|
|
1014
|
+
const maxIterations = limit !== undefined ? limit * 31 : 365 * 31;
|
|
1015
|
+
while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDay < to && iterations < maxIterations) {
|
|
1016
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
1017
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
1018
|
+
for (const clock of clocks) {
|
|
1019
|
+
const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
|
|
1020
|
+
if (zoned)
|
|
1021
|
+
recordCandidate(zoned);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
1025
|
+
iterations += 1;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Fallback for dayOfWeek with period/periodUnit but no explicit frequency/clocks
|
|
1029
|
+
if (enforceDayFilter && repeat.period && repeat.periodUnit) {
|
|
1030
|
+
const clocks = ["08:00:00"]; // Default to morning if no times specified
|
|
1031
|
+
let currentDayIter = startOfLocalDay(from, timeZone);
|
|
1032
|
+
let iter = 0;
|
|
1033
|
+
const maxIter = limit !== undefined ? limit * 31 : 365 * 31;
|
|
1034
|
+
while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDayIter < to && iter < maxIter) {
|
|
1035
|
+
const weekday = getLocalWeekday(currentDayIter, timeZone);
|
|
1036
|
+
if (dayFilter.has(weekday)) {
|
|
1037
|
+
for (const clock of clocks) {
|
|
1038
|
+
const zoned = makeZonedDateFromDay(currentDayIter, timeZone, clock);
|
|
1039
|
+
if (zoned)
|
|
1040
|
+
recordCandidate(zoned);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
currentDayIter = addLocalDays(currentDayIter, 1, timeZone);
|
|
1044
|
+
iter += 1;
|
|
1045
|
+
}
|
|
1046
|
+
return count;
|
|
1047
|
+
}
|
|
1048
|
+
return count;
|
|
1049
|
+
}
|
|
1050
|
+
function calculateTotalUnits(options) {
|
|
1051
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
1052
|
+
const { dosage, durationValue, durationUnit, roundToMultiple, context } = options;
|
|
1053
|
+
const from = coerceDate(options.from, "from");
|
|
1054
|
+
const providedConfig = options.config;
|
|
1055
|
+
const timeZone = (_a = options.timeZone) !== null && _a !== void 0 ? _a : providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.timeZone;
|
|
1056
|
+
if (!timeZone) {
|
|
1057
|
+
throw new Error("timeZone is required for calculateTotalUnits");
|
|
1058
|
+
}
|
|
1059
|
+
const eventClock = Object.assign(Object.assign({}, ((_b = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.eventClock) !== null && _b !== void 0 ? _b : {})), ((_c = options.eventClock) !== null && _c !== void 0 ? _c : {}));
|
|
1060
|
+
const mealOffsets = Object.assign(Object.assign({}, ((_d = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.mealOffsets) !== null && _d !== void 0 ? _d : {})), ((_e = options.mealOffsets) !== null && _e !== void 0 ? _e : {}));
|
|
1061
|
+
const frequencyDefaults = mergeFrequencyDefaults(providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.frequencyDefaults, options.frequencyDefaults);
|
|
1062
|
+
const config = {
|
|
1063
|
+
timeZone,
|
|
1064
|
+
eventClock,
|
|
1065
|
+
mealOffsets,
|
|
1066
|
+
frequencyDefaults
|
|
1067
|
+
};
|
|
1068
|
+
// Calculate end date based on duration
|
|
1069
|
+
let endDay;
|
|
1070
|
+
const dummyRepeat = { period: durationValue, periodUnit: durationUnit };
|
|
1071
|
+
const stepper = createIntervalStepper(dummyRepeat, timeZone);
|
|
1072
|
+
if (stepper) {
|
|
1073
|
+
endDay = stepper(from) || from;
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
endDay = from;
|
|
1077
|
+
}
|
|
1078
|
+
const count = countScheduleEvents(dosage, from, endDay, config, options.orderedAt ? coerceDate(options.orderedAt, "orderedAt") : from, options.orderedAt ? coerceDate(options.orderedAt, "orderedAt") : null, 2000);
|
|
1079
|
+
const doseQuantity = (_j = (_h = (_g = (_f = dosage.doseAndRate) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g.doseQuantity) === null || _h === void 0 ? void 0 : _h.value) !== null && _j !== void 0 ? _j : 0;
|
|
1080
|
+
let totalUnits = count * doseQuantity;
|
|
1081
|
+
if (roundToMultiple && roundToMultiple > 0) {
|
|
1082
|
+
totalUnits = Math.ceil(totalUnits / roundToMultiple) * roundToMultiple;
|
|
1083
|
+
}
|
|
1084
|
+
const result = { totalUnits };
|
|
1085
|
+
// Handle containers
|
|
1086
|
+
const containerValue = context === null || context === void 0 ? void 0 : context.containerValue;
|
|
1087
|
+
const containerUnit = context === null || context === void 0 ? void 0 : context.containerUnit;
|
|
1088
|
+
const doseUnit = (_m = (_l = (_k = dosage.doseAndRate) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.doseQuantity) === null || _m === void 0 ? void 0 : _m.unit;
|
|
1089
|
+
if (containerValue && containerValue > 0) {
|
|
1090
|
+
let effectiveUnits = totalUnits;
|
|
1091
|
+
if (containerUnit && doseUnit && containerUnit !== doseUnit) {
|
|
1092
|
+
let strength = context === null || context === void 0 ? void 0 : context.strengthRatio;
|
|
1093
|
+
if (!strength && (context === null || context === void 0 ? void 0 : context.strength)) {
|
|
1094
|
+
strength = (0, strength_1.parseStrengthIntoRatio)(context.strength, context) || undefined;
|
|
1095
|
+
}
|
|
1096
|
+
const converted = (0, units_1.convertValue)(totalUnits, doseUnit, containerUnit, strength);
|
|
1097
|
+
if (converted !== null) {
|
|
1098
|
+
effectiveUnits = converted;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
result.totalContainers = Math.ceil(effectiveUnits / containerValue);
|
|
1102
|
+
}
|
|
1103
|
+
return result;
|
|
1104
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -279,6 +279,12 @@ export type RouteCode = SNOMEDCTRouteCodes;
|
|
|
279
279
|
export declare const RouteCode: typeof SNOMEDCTRouteCodes;
|
|
280
280
|
export interface MedicationContext {
|
|
281
281
|
dosageForm?: string;
|
|
282
|
+
/** "Simple" strength string; might be the only way strength is provided
|
|
283
|
+
* for discrete units this is the amount of medication for unit e.g. "500 mg" (1 tablet), or for mixed tablets might be like "400 mg + 80 mg"
|
|
284
|
+
* for things like creams or fluids or syrupsit might be "2%", 5 g/100g, 100mg/ 100 g, 262 mg/15 mL, 200 mg/2 mL, 1 mg/dL, or "400 mg/5mL + 80 mg/5mL"
|
|
285
|
+
* In the "x + y" case, the strengthQuantity/strengthRatio will be the sum of the two ingredients.
|
|
286
|
+
*/
|
|
287
|
+
strength?: string;
|
|
282
288
|
strengthQuantity?: FhirQuantity;
|
|
283
289
|
strengthRatio?: FhirRatio;
|
|
284
290
|
strengthCodeableConcept?: FhirCodeableConcept;
|
|
@@ -527,6 +533,22 @@ export interface ParseResult {
|
|
|
527
533
|
}>;
|
|
528
534
|
};
|
|
529
535
|
}
|
|
536
|
+
export interface LintIssue {
|
|
537
|
+
/** Human-readable description of why the segment could not be parsed. */
|
|
538
|
+
message: string;
|
|
539
|
+
/** Original substring that triggered the issue. */
|
|
540
|
+
text: string;
|
|
541
|
+
/** Tokens contributing to the unparsed segment. */
|
|
542
|
+
tokens: string[];
|
|
543
|
+
/** Location of {@link text} relative to the caller's original input. */
|
|
544
|
+
range?: TextRange;
|
|
545
|
+
}
|
|
546
|
+
export interface LintResult {
|
|
547
|
+
/** Standard parse output including FHIR representation and metadata. */
|
|
548
|
+
result: ParseResult;
|
|
549
|
+
/** Segments of the input that could not be interpreted. */
|
|
550
|
+
issues: LintIssue[];
|
|
551
|
+
}
|
|
530
552
|
/**
|
|
531
553
|
* Maps EventTiming codes (or other institution-specific timing strings) to
|
|
532
554
|
* 24-hour clock representations such as "08:00".
|
|
@@ -568,3 +590,14 @@ export interface NextDueDoseOptions {
|
|
|
568
590
|
frequencyDefaults?: FrequencyFallbackTimes;
|
|
569
591
|
config?: NextDueDoseConfig;
|
|
570
592
|
}
|
|
593
|
+
export interface TotalUnitsResult {
|
|
594
|
+
totalUnits: number;
|
|
595
|
+
totalContainers?: number;
|
|
596
|
+
}
|
|
597
|
+
export interface TotalUnitsOptions extends NextDueDoseOptions {
|
|
598
|
+
dosage: FhirDosage;
|
|
599
|
+
durationValue: number;
|
|
600
|
+
durationUnit: FhirPeriodUnit;
|
|
601
|
+
roundToMultiple?: number;
|
|
602
|
+
context?: MedicationContext;
|
|
603
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FhirRatio, FhirQuantity, MedicationContext } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* High-level strength parser that returns the most appropriate FHIR representation.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseStrength(strength: string, context?: MedicationContext): {
|
|
6
|
+
strengthQuantity?: FhirQuantity;
|
|
7
|
+
strengthRatio?: FhirRatio;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Internal helper to parse a strength string into a FHIR Ratio.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseStrengthIntoRatio(strength: string, context?: MedicationContext): FhirRatio | null;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStrength = parseStrength;
|
|
4
|
+
exports.parseStrengthIntoRatio = parseStrengthIntoRatio;
|
|
5
|
+
const units_1 = require("./units");
|
|
6
|
+
const maps_1 = require("../maps");
|
|
7
|
+
const array_1 = require("./array");
|
|
8
|
+
/**
|
|
9
|
+
* High-level strength parser that returns the most appropriate FHIR representation.
|
|
10
|
+
*/
|
|
11
|
+
function parseStrength(strength, context) {
|
|
12
|
+
var _a, _b, _c;
|
|
13
|
+
const ratio = parseStrengthIntoRatio(strength, context);
|
|
14
|
+
if (!ratio)
|
|
15
|
+
return {};
|
|
16
|
+
if (((_a = ratio.denominator) === null || _a === void 0 ? void 0 : _a.value) === 1 && (((_b = ratio.denominator) === null || _b === void 0 ? void 0 : _b.unit) === "unit" || !((_c = ratio.denominator) === null || _c === void 0 ? void 0 : _c.unit))) {
|
|
17
|
+
return { strengthQuantity: ratio.numerator };
|
|
18
|
+
}
|
|
19
|
+
return { strengthRatio: ratio };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Internal helper to parse a strength string into a FHIR Ratio.
|
|
23
|
+
*/
|
|
24
|
+
function parseStrengthIntoRatio(strength, context) {
|
|
25
|
+
var _a, _b, _c, _d, _e;
|
|
26
|
+
const parts = strength.split("+").map((p) => p.trim());
|
|
27
|
+
let totalMgPerMlOrG = 0;
|
|
28
|
+
let hasVolume = false;
|
|
29
|
+
let hasWeightDenominator = false;
|
|
30
|
+
let targetNumUnit;
|
|
31
|
+
let targetDenUnit;
|
|
32
|
+
let targetDenValue;
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
const ratio = parseSingleStrengthPart(part, context);
|
|
35
|
+
if (!ratio || !((_a = ratio.numerator) === null || _a === void 0 ? void 0 : _a.value) || ratio.numerator.value === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const nUnit = ratio.numerator.unit;
|
|
38
|
+
const dUnit = ((_b = ratio.denominator) === null || _b === void 0 ? void 0 : _b.unit) || "unit";
|
|
39
|
+
const dCat = (0, units_1.getUnitCategory)(dUnit);
|
|
40
|
+
// Save target units from the first part
|
|
41
|
+
if (!targetNumUnit)
|
|
42
|
+
targetNumUnit = nUnit;
|
|
43
|
+
if (!targetDenUnit)
|
|
44
|
+
targetDenUnit = dUnit;
|
|
45
|
+
if (targetDenValue === undefined)
|
|
46
|
+
targetDenValue = (_c = ratio.denominator) === null || _c === void 0 ? void 0 : _c.value;
|
|
47
|
+
const nValue = ratio.numerator.value;
|
|
48
|
+
const dValue = (_e = (_d = ratio.denominator) === null || _d === void 0 ? void 0 : _d.value) !== null && _e !== void 0 ? _e : 1;
|
|
49
|
+
const nFactor = (0, units_1.getBaseUnitFactor)(nUnit);
|
|
50
|
+
const dFactor = (dCat === "volume" || dCat === "mass") ? (0, units_1.getBaseUnitFactor)(dUnit) : 1;
|
|
51
|
+
totalMgPerMlOrG += (nValue * nFactor) / (dValue * dFactor);
|
|
52
|
+
if (dCat === "volume")
|
|
53
|
+
hasVolume = true;
|
|
54
|
+
if (dCat === "mass")
|
|
55
|
+
hasWeightDenominator = true;
|
|
56
|
+
}
|
|
57
|
+
if (totalMgPerMlOrG === 0)
|
|
58
|
+
return null;
|
|
59
|
+
const isComposite = parts.length > 1;
|
|
60
|
+
const resultNumUnit = isComposite ? "mg" : (targetNumUnit || "mg");
|
|
61
|
+
let resultDenUnit = targetDenUnit || "unit";
|
|
62
|
+
if (isComposite) {
|
|
63
|
+
if (hasVolume)
|
|
64
|
+
resultDenUnit = "mL";
|
|
65
|
+
else if (hasWeightDenominator)
|
|
66
|
+
resultDenUnit = "g";
|
|
67
|
+
else
|
|
68
|
+
resultDenUnit = "unit";
|
|
69
|
+
}
|
|
70
|
+
const resultDenValue = isComposite ? 1 : (targetDenValue !== null && targetDenValue !== void 0 ? targetDenValue : 1);
|
|
71
|
+
const denCat = (0, units_1.getUnitCategory)(resultDenUnit);
|
|
72
|
+
const dBaseFactor = (denCat === "volume" || denCat === "mass")
|
|
73
|
+
? (0, units_1.getBaseUnitFactor)(resultDenUnit)
|
|
74
|
+
: 1;
|
|
75
|
+
const totalBaseDenominatorValue = resultDenValue * dBaseFactor;
|
|
76
|
+
const totalNumeratorMg = totalMgPerMlOrG * totalBaseDenominatorValue;
|
|
77
|
+
const finalNumValue = totalNumeratorMg / (0, units_1.getBaseUnitFactor)(resultNumUnit);
|
|
78
|
+
return {
|
|
79
|
+
numerator: { value: finalNumValue, unit: resultNumUnit },
|
|
80
|
+
denominator: { value: resultDenValue, unit: resultDenUnit }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function isSolidDosageForm(form) {
|
|
84
|
+
const normalized = form.toLowerCase().trim();
|
|
85
|
+
// 1. Get the default unit for this form
|
|
86
|
+
let unit = maps_1.DEFAULT_UNIT_BY_NORMALIZED_FORM[normalized];
|
|
87
|
+
if (!unit) {
|
|
88
|
+
const mapped = maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE[normalized];
|
|
89
|
+
if (mapped) {
|
|
90
|
+
unit = maps_1.DEFAULT_UNIT_BY_NORMALIZED_FORM[mapped];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 2. Identify if it's explicitly a liquid/gas unit
|
|
94
|
+
const liquidUnits = ["ml", "spray", "puff", "drop"];
|
|
95
|
+
if (unit) {
|
|
96
|
+
const u = unit.toLowerCase();
|
|
97
|
+
if ((0, array_1.arrayIncludes)(liquidUnits, u))
|
|
98
|
+
return false;
|
|
99
|
+
// Any other mapped unit (g, tab, cap, etc.) is considered solid for % purpose
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// 3. Keyword-based heuristics as fallback
|
|
103
|
+
const solidKeywords = [
|
|
104
|
+
"tablet", "capsule", "patch", "cream", "ointment", "gel", "paste",
|
|
105
|
+
"suppositor", "powder", "lozenge", "patch", "stick", "implant",
|
|
106
|
+
"piece", "granule", "lozenge", "pessary"
|
|
107
|
+
];
|
|
108
|
+
for (let i = 0; i < solidKeywords.length; i++) {
|
|
109
|
+
if (normalized.indexOf(solidKeywords[i]) !== -1)
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function parseSingleStrengthPart(part, context) {
|
|
115
|
+
var _a, _b;
|
|
116
|
+
const p = part.trim();
|
|
117
|
+
// 1. Percentage
|
|
118
|
+
const percentMatch = p.match(/^(\d+(?:\.\d+)?)\s*%$/);
|
|
119
|
+
if (percentMatch) {
|
|
120
|
+
const isSolid = (context === null || context === void 0 ? void 0 : context.dosageForm) && isSolidDosageForm(context.dosageForm);
|
|
121
|
+
return {
|
|
122
|
+
numerator: { value: parseFloat(percentMatch[1]), unit: "g" },
|
|
123
|
+
denominator: { value: 100, unit: isSolid ? "g" : "mL" }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// 2. Ratio
|
|
127
|
+
const ratioMatch = p.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z0-9%]+)?\s*\/\s*(\d+(?:\.\d+)?)?\s*([a-zA-Z0-9%]+)$/);
|
|
128
|
+
if (ratioMatch) {
|
|
129
|
+
return {
|
|
130
|
+
numerator: {
|
|
131
|
+
value: parseFloat(ratioMatch[1]),
|
|
132
|
+
unit: ((_a = ratioMatch[2]) === null || _a === void 0 ? void 0 : _a.trim()) || "mg"
|
|
133
|
+
},
|
|
134
|
+
denominator: {
|
|
135
|
+
value: ratioMatch[3] ? parseFloat(ratioMatch[3]) : 1,
|
|
136
|
+
unit: (_b = ratioMatch[4]) === null || _b === void 0 ? void 0 : _b.trim()
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 3. Simple Quantity
|
|
141
|
+
const quantityMatch = p.match(/^(\d+(?:\.\d+)?)\s*([a-zA-Z0-9%]+)$/);
|
|
142
|
+
if (quantityMatch) {
|
|
143
|
+
return {
|
|
144
|
+
numerator: { value: parseFloat(quantityMatch[1]), unit: quantityMatch[2].trim() },
|
|
145
|
+
denominator: { value: 1, unit: "unit" }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const MASS_UNITS: Record<string, number>;
|
|
2
|
+
export declare const VOLUME_UNITS: Record<string, number>;
|
|
3
|
+
export declare function getUnitCategory(unit?: string): "mass" | "volume" | "other";
|
|
4
|
+
export declare function getBaseUnitFactor(unit?: string): number;
|
|
5
|
+
export declare function convertValue(value: number, fromUnit: string, toUnit: string, strength?: {
|
|
6
|
+
numerator: {
|
|
7
|
+
value: number;
|
|
8
|
+
unit: string;
|
|
9
|
+
};
|
|
10
|
+
denominator: {
|
|
11
|
+
value: number;
|
|
12
|
+
unit: string;
|
|
13
|
+
};
|
|
14
|
+
}): number | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VOLUME_UNITS = exports.MASS_UNITS = void 0;
|
|
4
|
+
exports.getUnitCategory = getUnitCategory;
|
|
5
|
+
exports.getBaseUnitFactor = getBaseUnitFactor;
|
|
6
|
+
exports.convertValue = convertValue;
|
|
7
|
+
exports.MASS_UNITS = {
|
|
8
|
+
kg: 1000000,
|
|
9
|
+
g: 1000,
|
|
10
|
+
mg: 1,
|
|
11
|
+
mcg: 0.001,
|
|
12
|
+
ug: 0.001,
|
|
13
|
+
microg: 0.001,
|
|
14
|
+
ng: 0.000001
|
|
15
|
+
};
|
|
16
|
+
exports.VOLUME_UNITS = {
|
|
17
|
+
l: 1000,
|
|
18
|
+
dl: 100,
|
|
19
|
+
ml: 1,
|
|
20
|
+
ul: 0.001,
|
|
21
|
+
microl: 0.001,
|
|
22
|
+
cm3: 1,
|
|
23
|
+
tsp: 5,
|
|
24
|
+
tbsp: 15
|
|
25
|
+
};
|
|
26
|
+
function getUnitCategory(unit) {
|
|
27
|
+
if (!unit)
|
|
28
|
+
return "other";
|
|
29
|
+
const u = unit.toLowerCase();
|
|
30
|
+
if (exports.MASS_UNITS[u] !== undefined)
|
|
31
|
+
return "mass";
|
|
32
|
+
if (exports.VOLUME_UNITS[u] !== undefined)
|
|
33
|
+
return "volume";
|
|
34
|
+
return "other";
|
|
35
|
+
}
|
|
36
|
+
function getBaseUnitFactor(unit) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
if (!unit)
|
|
39
|
+
return 1;
|
|
40
|
+
const u = unit.toLowerCase();
|
|
41
|
+
return (_b = (_a = exports.MASS_UNITS[u]) !== null && _a !== void 0 ? _a : exports.VOLUME_UNITS[u]) !== null && _b !== void 0 ? _b : 1;
|
|
42
|
+
}
|
|
43
|
+
function convertValue(value, fromUnit, toUnit, strength) {
|
|
44
|
+
const f = fromUnit.toLowerCase();
|
|
45
|
+
const t = toUnit.toLowerCase();
|
|
46
|
+
if (f === t)
|
|
47
|
+
return value;
|
|
48
|
+
const fCat = getUnitCategory(f);
|
|
49
|
+
const tCat = getUnitCategory(t);
|
|
50
|
+
// 1. Same category conversion
|
|
51
|
+
if (fCat === tCat && fCat !== "other") {
|
|
52
|
+
const fFactor = getBaseUnitFactor(f);
|
|
53
|
+
const tFactor = getBaseUnitFactor(t);
|
|
54
|
+
return (value * fFactor) / tFactor;
|
|
55
|
+
}
|
|
56
|
+
// 2. Cross-category conversion using strength
|
|
57
|
+
if (strength && ((fCat === "mass" && tCat === "volume") || (fCat === "volume" && tCat === "mass"))) {
|
|
58
|
+
const numUnit = strength.numerator.unit.toLowerCase();
|
|
59
|
+
const denUnit = strength.denominator.unit.toLowerCase();
|
|
60
|
+
const numCat = getUnitCategory(numUnit);
|
|
61
|
+
const denCat = getUnitCategory(denUnit);
|
|
62
|
+
if (numCat !== denCat && numCat !== "other" && denCat !== "other") {
|
|
63
|
+
const massSide = numCat === "mass" ? strength.numerator : strength.denominator;
|
|
64
|
+
const volSide = numCat === "volume" ? strength.numerator : strength.denominator;
|
|
65
|
+
// Normalize bridge to base units (mg/mL)
|
|
66
|
+
const bridgeDensity = (massSide.value * getBaseUnitFactor(massSide.unit)) / (volSide.value * getBaseUnitFactor(volSide.unit));
|
|
67
|
+
if (fCat === "mass") {
|
|
68
|
+
// Mass to Volume: value_mg / density_mg_per_ml
|
|
69
|
+
const valueMg = value * getBaseUnitFactor(fromUnit);
|
|
70
|
+
const valueMl = valueMg / bridgeDensity;
|
|
71
|
+
return valueMl / getBaseUnitFactor(toUnit);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Volume to Mass: value_ml * density_mg_per_ml
|
|
75
|
+
const valueMl = value * getBaseUnitFactor(fromUnit);
|
|
76
|
+
const valueMg = valueMl * bridgeDensity;
|
|
77
|
+
return valueMg / getBaseUnitFactor(toUnit);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
package/package.json
CHANGED
package/dist/utils/enum.d.ts
DELETED