ezmedicationinput 0.1.34 → 0.1.36
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 +62 -14
- package/dist/index.d.ts +6 -4
- package/dist/index.js +208 -25
- package/dist/parser.js +3 -0
- package/dist/schedule.js +145 -79
- package/dist/segment.d.ts +6 -0
- package/dist/segment.js +203 -0
- package/dist/types.d.ts +77 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Converts shorthand strings (e.g. `1x3 po pc`, `500 mg po q6h prn pain`) into FHIR-compliant dosage JSON.
|
|
8
|
+
- Parses multi-clause sigs into multiple dosage items (e.g. `OD ... , OS ...`) while preserving a first-item compatibility shape for legacy single-dose consumers.
|
|
8
9
|
- Emits timing abbreviations (`timing.code`) and repeat structures simultaneously where possible.
|
|
9
10
|
- Maps meal/time blocks to the correct `Timing.repeat.when` **EventTiming** codes and can auto-expand AC/PC/C into specific meals.
|
|
10
11
|
- Outputs SNOMED CT route codings (while providing friendly text) and round-trips known SNOMED routes back into the parser.
|
|
@@ -28,29 +29,57 @@ npm install ezmedicationinput
|
|
|
28
29
|
```ts
|
|
29
30
|
import { parseSig } from "ezmedicationinput";
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
const batch = parseSig("1x3 po pc", { context: { dosageForm: "tab" } });
|
|
33
|
+
|
|
34
|
+
// New API
|
|
35
|
+
console.log(batch.count); // 1
|
|
36
|
+
console.log(batch.items[0].fhir);
|
|
37
|
+
|
|
38
|
+
// Legacy compatibility (first parsed item)
|
|
39
|
+
console.log(batch.fhir);
|
|
33
40
|
```
|
|
34
41
|
|
|
35
42
|
Example output:
|
|
36
43
|
|
|
37
44
|
```json
|
|
38
45
|
{
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
"count": 1,
|
|
47
|
+
"items": [
|
|
48
|
+
{
|
|
49
|
+
"fhir": {
|
|
50
|
+
"text": "Take 1 tablet by mouth three times daily after meals.",
|
|
51
|
+
"timing": {
|
|
52
|
+
"code": { "coding": [{ "code": "TID" }], "text": "TID" },
|
|
53
|
+
"repeat": {
|
|
54
|
+
"frequency": 3,
|
|
55
|
+
"period": 1,
|
|
56
|
+
"periodUnit": "d",
|
|
57
|
+
"when": ["PC"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"route": { "text": "by mouth" },
|
|
61
|
+
"doseAndRate": [{ "doseQuantity": { "value": 1, "unit": "tab" } }]
|
|
62
|
+
}
|
|
47
63
|
}
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"doseAndRate": [{ "doseQuantity": { "value": 1, "unit": "tab" } }]
|
|
64
|
+
],
|
|
65
|
+
"fhir": { "...": "same as items[0].fhir for compatibility" }
|
|
51
66
|
}
|
|
52
67
|
```
|
|
53
68
|
|
|
69
|
+
### Multi-clause parsing and legacy compatibility
|
|
70
|
+
|
|
71
|
+
`parseSig` / `parseSigAsync` return a **batch** object:
|
|
72
|
+
|
|
73
|
+
- `count`: number of parsed dosage clauses
|
|
74
|
+
- `items`: array of full parse results (one per clause)
|
|
75
|
+
- `meta.segments`: source ranges for each clause
|
|
76
|
+
|
|
77
|
+
For single-dose integrations that haven't migrated yet, the batch also keeps legacy first-item fields:
|
|
78
|
+
|
|
79
|
+
- `fhir`, `shortText`, `longText`, `warnings`, `meta`, and for linting `result`/`issues`
|
|
80
|
+
|
|
81
|
+
So existing code that expects one result can continue using first-item compatibility while newer code uses `items[]`.
|
|
82
|
+
|
|
54
83
|
### PRN reasons & additional instructions
|
|
55
84
|
|
|
56
85
|
`parseSig` identifies PRN (as-needed) clauses and trailing instructions, then
|
|
@@ -101,9 +130,28 @@ rendering.
|
|
|
101
130
|
|
|
102
131
|
When a PRN reason cannot be auto-resolved, any registered suggestion resolvers
|
|
103
132
|
are invoked and their responses are surfaced through
|
|
104
|
-
`
|
|
133
|
+
`ParseBatchResult.items[n].meta.prnReasonLookups` so client applications can prompt the user
|
|
105
134
|
to choose a coded concept.
|
|
106
135
|
|
|
136
|
+
### Formatting multi-item results back to sig text
|
|
137
|
+
|
|
138
|
+
Use either helper depending on your source:
|
|
139
|
+
|
|
140
|
+
- `formatParseBatch(batch, style?, separator?)` when you already have `parseSig` output.
|
|
141
|
+
- `formatSigBatch(dosages, style?, { separator })` when you have an array of FHIR `Dosage` entries.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { formatParseBatch, formatSigBatch, parseSig } from "ezmedicationinput";
|
|
145
|
+
|
|
146
|
+
const batch = parseSig("1 tab po @ 8:00, 2 tabs po with lunch, 1 tab before dinner, 4 tabs po hs");
|
|
147
|
+
|
|
148
|
+
const shortSig = formatParseBatch(batch, "short");
|
|
149
|
+
// => "1 tab PO 08:00, 2 tab PO CD, 1 tab PO ACV, 4 tab PO HS"
|
|
150
|
+
|
|
151
|
+
const shortFromFhir = formatSigBatch(batch.items.map((item) => item.fhir), "short");
|
|
152
|
+
// => same combined short sig text
|
|
153
|
+
```
|
|
154
|
+
|
|
107
155
|
### Sig (directions) suggestions
|
|
108
156
|
|
|
109
157
|
Use `suggestSig` to drive autocomplete experiences while the clinician is
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FhirDosage, FormatOptions,
|
|
1
|
+
import { FhirDosage, FormatBatchOptions, FormatOptions, LintBatchResult, ParseBatchResult, ParseOptions, ParseResult } from "./types";
|
|
2
2
|
export { parseInternal } from "./parser";
|
|
3
3
|
export { suggestSig } from "./suggest";
|
|
4
4
|
export * from "./types";
|
|
@@ -7,8 +7,10 @@ export { parseStrength, parseStrengthIntoRatio } from "./utils/strength";
|
|
|
7
7
|
export { getRegisteredSigLocalizations, registerSigLocalization, resolveSigLocalization, resolveSigTranslation } from "./i18n";
|
|
8
8
|
export type { SigLocalization, SigLocalizationConfig, SigTranslation, SigTranslationConfig } from "./i18n";
|
|
9
9
|
export { DEFAULT_BODY_SITE_SNOMED, DEFAULT_BODY_SITE_SNOMED_SOURCE, DEFAULT_ROUTE_SYNONYMS, DEFAULT_UNIT_BY_ROUTE, KNOWN_DOSAGE_FORMS_TO_DOSE } from './maps';
|
|
10
|
-
export declare function parseSig(input: string, options?: ParseOptions):
|
|
11
|
-
export declare function lintSig(input: string, options?: ParseOptions):
|
|
12
|
-
export declare function parseSigAsync(input: string, options?: ParseOptions): Promise<
|
|
10
|
+
export declare function parseSig(input: string, options?: ParseOptions): ParseBatchResult;
|
|
11
|
+
export declare function lintSig(input: string, options?: ParseOptions): LintBatchResult;
|
|
12
|
+
export declare function parseSigAsync(input: string, options?: ParseOptions): Promise<ParseBatchResult>;
|
|
13
13
|
export declare function formatSig(dosage: FhirDosage, style?: "short" | "long", options?: FormatOptions): string;
|
|
14
|
+
export declare function formatSigBatch(dosages: FhirDosage[], style?: "short" | "long", options?: FormatBatchOptions): string;
|
|
15
|
+
export declare function formatParseBatch(batch: ParseBatchResult, style?: "short" | "long", separator?: string): string;
|
|
14
16
|
export declare function fromFhirDosage(dosage: FhirDosage, options?: FormatOptions): ParseResult;
|
package/dist/index.js
CHANGED
|
@@ -28,11 +28,14 @@ exports.parseSig = parseSig;
|
|
|
28
28
|
exports.lintSig = lintSig;
|
|
29
29
|
exports.parseSigAsync = parseSigAsync;
|
|
30
30
|
exports.formatSig = formatSig;
|
|
31
|
+
exports.formatSigBatch = formatSigBatch;
|
|
32
|
+
exports.formatParseBatch = formatParseBatch;
|
|
31
33
|
exports.fromFhirDosage = fromFhirDosage;
|
|
32
34
|
const format_1 = require("./format");
|
|
33
35
|
const fhir_1 = require("./fhir");
|
|
34
36
|
const i18n_1 = require("./i18n");
|
|
35
37
|
const parser_1 = require("./parser");
|
|
38
|
+
const segment_1 = require("./segment");
|
|
36
39
|
var parser_2 = require("./parser");
|
|
37
40
|
Object.defineProperty(exports, "parseInternal", { enumerable: true, get: function () { return parser_2.parseInternal; } });
|
|
38
41
|
var suggest_1 = require("./suggest");
|
|
@@ -55,37 +58,104 @@ Object.defineProperty(exports, "DEFAULT_BODY_SITE_SNOMED_SOURCE", { enumerable:
|
|
|
55
58
|
Object.defineProperty(exports, "DEFAULT_ROUTE_SYNONYMS", { enumerable: true, get: function () { return maps_1.DEFAULT_ROUTE_SYNONYMS; } });
|
|
56
59
|
Object.defineProperty(exports, "DEFAULT_UNIT_BY_ROUTE", { enumerable: true, get: function () { return maps_1.DEFAULT_UNIT_BY_ROUTE; } });
|
|
57
60
|
Object.defineProperty(exports, "KNOWN_DOSAGE_FORMS_TO_DOSE", { enumerable: true, get: function () { return maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE; } });
|
|
61
|
+
function toSegmentMeta(segments) {
|
|
62
|
+
return segments.map((segment, index) => ({
|
|
63
|
+
index,
|
|
64
|
+
text: segment.text,
|
|
65
|
+
range: { start: segment.start, end: segment.end }
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
58
68
|
function parseSig(input, options) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const segments = (0, segment_1.splitSigSegments)(input);
|
|
70
|
+
const carry = {};
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const segment of segments) {
|
|
73
|
+
const internal = (0, parser_1.parseInternal)(segment.text, options);
|
|
74
|
+
applyCarryForward(internal, carry);
|
|
75
|
+
(0, parser_1.applyPrnReasonCoding)(internal, options);
|
|
76
|
+
(0, parser_1.applySiteCoding)(internal, options);
|
|
77
|
+
const result = buildParseResult(internal, options);
|
|
78
|
+
rebaseParseResult(result, input, segment.start);
|
|
79
|
+
results.push(result);
|
|
80
|
+
updateCarryForward(carry, internal);
|
|
81
|
+
}
|
|
82
|
+
const legacy = resolveLegacyParseResult(results, input, options);
|
|
83
|
+
return {
|
|
84
|
+
input,
|
|
85
|
+
count: results.length,
|
|
86
|
+
items: results,
|
|
87
|
+
fhir: legacy.fhir,
|
|
88
|
+
shortText: legacy.shortText,
|
|
89
|
+
longText: legacy.longText,
|
|
90
|
+
warnings: legacy.warnings,
|
|
91
|
+
meta: Object.assign(Object.assign({}, legacy.meta), { segments: toSegmentMeta(segments) })
|
|
92
|
+
};
|
|
63
93
|
}
|
|
64
94
|
function lintSig(input, options) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
const segments = (0, segment_1.splitSigSegments)(input);
|
|
96
|
+
const carry = {};
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const segment of segments) {
|
|
99
|
+
const internal = (0, parser_1.parseInternal)(segment.text, options);
|
|
100
|
+
applyCarryForward(internal, carry);
|
|
101
|
+
(0, parser_1.applyPrnReasonCoding)(internal, options);
|
|
102
|
+
(0, parser_1.applySiteCoding)(internal, options);
|
|
103
|
+
const result = buildParseResult(internal, options);
|
|
104
|
+
rebaseParseResult(result, input, segment.start);
|
|
105
|
+
const groups = (0, parser_1.findUnparsedTokenGroups)(internal);
|
|
106
|
+
const issues = groups.map((group) => {
|
|
107
|
+
const shiftedRange = shiftRange(group.range, segment.start);
|
|
108
|
+
const text = shiftedRange
|
|
109
|
+
? input.slice(shiftedRange.start, shiftedRange.end)
|
|
110
|
+
: group.tokens.map((token) => token.original).join(" ");
|
|
111
|
+
return {
|
|
112
|
+
message: "Unrecognized text",
|
|
113
|
+
text: text.trim() || text,
|
|
114
|
+
tokens: group.tokens.map((token) => token.original),
|
|
115
|
+
range: shiftedRange
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
results.push({ result, issues });
|
|
119
|
+
updateCarryForward(carry, internal);
|
|
120
|
+
}
|
|
121
|
+
const legacy = resolveLegacyLintResult(results, input, options);
|
|
122
|
+
return {
|
|
123
|
+
input,
|
|
124
|
+
count: results.length,
|
|
125
|
+
items: results,
|
|
126
|
+
result: legacy.result,
|
|
127
|
+
issues: legacy.issues,
|
|
128
|
+
meta: {
|
|
129
|
+
segments: toSegmentMeta(segments)
|
|
130
|
+
}
|
|
131
|
+
};
|
|
82
132
|
}
|
|
83
133
|
function parseSigAsync(input, options) {
|
|
84
134
|
return __awaiter(this, void 0, void 0, function* () {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
135
|
+
const segments = (0, segment_1.splitSigSegments)(input);
|
|
136
|
+
const carry = {};
|
|
137
|
+
const results = [];
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
const internal = (0, parser_1.parseInternal)(segment.text, options);
|
|
140
|
+
applyCarryForward(internal, carry);
|
|
141
|
+
yield (0, parser_1.applyPrnReasonCodingAsync)(internal, options);
|
|
142
|
+
yield (0, parser_1.applySiteCodingAsync)(internal, options);
|
|
143
|
+
const result = buildParseResult(internal, options);
|
|
144
|
+
rebaseParseResult(result, input, segment.start);
|
|
145
|
+
results.push(result);
|
|
146
|
+
updateCarryForward(carry, internal);
|
|
147
|
+
}
|
|
148
|
+
const legacy = resolveLegacyParseResult(results, input, options);
|
|
149
|
+
return {
|
|
150
|
+
input,
|
|
151
|
+
count: results.length,
|
|
152
|
+
items: results,
|
|
153
|
+
fhir: legacy.fhir,
|
|
154
|
+
shortText: legacy.shortText,
|
|
155
|
+
longText: legacy.longText,
|
|
156
|
+
warnings: legacy.warnings,
|
|
157
|
+
meta: Object.assign(Object.assign({}, legacy.meta), { segments: toSegmentMeta(segments) })
|
|
158
|
+
};
|
|
89
159
|
});
|
|
90
160
|
}
|
|
91
161
|
function formatSig(dosage, style = "short", options) {
|
|
@@ -93,6 +163,24 @@ function formatSig(dosage, style = "short", options) {
|
|
|
93
163
|
const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
|
|
94
164
|
return (0, format_1.formatInternal)(internal, style, localization);
|
|
95
165
|
}
|
|
166
|
+
function formatSigBatch(dosages, style = "short", options) {
|
|
167
|
+
var _a;
|
|
168
|
+
const separator = (_a = options === null || options === void 0 ? void 0 : options.separator) !== null && _a !== void 0 ? _a : ", ";
|
|
169
|
+
const formatted = [];
|
|
170
|
+
for (const dosage of dosages) {
|
|
171
|
+
const text = formatSig(dosage, style, options);
|
|
172
|
+
if (text.trim()) {
|
|
173
|
+
formatted.push(text);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return formatted.join(separator);
|
|
177
|
+
}
|
|
178
|
+
function formatParseBatch(batch, style = "short", separator = ", ") {
|
|
179
|
+
const texts = batch.items
|
|
180
|
+
.map((item) => (style === "short" ? item.shortText : item.longText))
|
|
181
|
+
.filter((text) => typeof text === "string" && text.trim().length > 0);
|
|
182
|
+
return texts.join(separator);
|
|
183
|
+
}
|
|
96
184
|
function fromFhirDosage(dosage, options) {
|
|
97
185
|
var _a, _b, _c, _d, _e, _f;
|
|
98
186
|
const internal = (0, fhir_1.internalFromFhir)(dosage);
|
|
@@ -255,3 +343,98 @@ function buildParseResult(internal, options) {
|
|
|
255
343
|
}
|
|
256
344
|
};
|
|
257
345
|
}
|
|
346
|
+
function applyCarryForward(internal, carry) {
|
|
347
|
+
if (!internal.routeCode && !internal.routeText) {
|
|
348
|
+
if (carry.routeCode) {
|
|
349
|
+
internal.routeCode = carry.routeCode;
|
|
350
|
+
}
|
|
351
|
+
if (!internal.routeText && carry.routeText) {
|
|
352
|
+
internal.routeText = carry.routeText;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!internal.unit && carry.unit) {
|
|
356
|
+
internal.unit = carry.unit;
|
|
357
|
+
}
|
|
358
|
+
if (internal.dose === undefined &&
|
|
359
|
+
internal.doseRange === undefined &&
|
|
360
|
+
carry.dose !== undefined &&
|
|
361
|
+
internal.unit &&
|
|
362
|
+
internal.unit === carry.unit) {
|
|
363
|
+
internal.dose = carry.dose;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function updateCarryForward(carry, internal) {
|
|
367
|
+
if (internal.routeCode) {
|
|
368
|
+
carry.routeCode = internal.routeCode;
|
|
369
|
+
}
|
|
370
|
+
if (internal.routeText) {
|
|
371
|
+
carry.routeText = internal.routeText;
|
|
372
|
+
}
|
|
373
|
+
if (internal.unit) {
|
|
374
|
+
carry.unit = internal.unit;
|
|
375
|
+
}
|
|
376
|
+
if (internal.dose !== undefined) {
|
|
377
|
+
carry.dose = internal.dose;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function rebaseParseResult(result, fullInput, offset) {
|
|
381
|
+
const rebaseRequest = (request) => {
|
|
382
|
+
request.inputText = fullInput;
|
|
383
|
+
if (request.range) {
|
|
384
|
+
request.range = shiftRange(request.range, offset);
|
|
385
|
+
if (request.range) {
|
|
386
|
+
request.sourceText = fullInput.slice(request.range.start, request.range.end);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
if (result.meta.siteLookups) {
|
|
391
|
+
for (const lookup of result.meta.siteLookups) {
|
|
392
|
+
rebaseRequest(lookup.request);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (result.meta.prnReasonLookups) {
|
|
396
|
+
for (const lookup of result.meta.prnReasonLookups) {
|
|
397
|
+
rebaseRequest(lookup.request);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function shiftRange(range, offset) {
|
|
402
|
+
if (!range) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
start: range.start + offset,
|
|
407
|
+
end: range.end + offset
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function resolveLegacyParseResult(results, input, options) {
|
|
411
|
+
if (results.length > 0) {
|
|
412
|
+
return results[0];
|
|
413
|
+
}
|
|
414
|
+
const internal = (0, parser_1.parseInternal)(input, options);
|
|
415
|
+
(0, parser_1.applyPrnReasonCoding)(internal, options);
|
|
416
|
+
(0, parser_1.applySiteCoding)(internal, options);
|
|
417
|
+
return buildParseResult(internal, options);
|
|
418
|
+
}
|
|
419
|
+
function resolveLegacyLintResult(results, input, options) {
|
|
420
|
+
if (results.length > 0) {
|
|
421
|
+
return results[0];
|
|
422
|
+
}
|
|
423
|
+
const internal = (0, parser_1.parseInternal)(input, options);
|
|
424
|
+
(0, parser_1.applyPrnReasonCoding)(internal, options);
|
|
425
|
+
(0, parser_1.applySiteCoding)(internal, options);
|
|
426
|
+
const result = buildParseResult(internal, options);
|
|
427
|
+
const groups = (0, parser_1.findUnparsedTokenGroups)(internal);
|
|
428
|
+
const issues = groups.map((group) => {
|
|
429
|
+
const text = group.range
|
|
430
|
+
? internal.input.slice(group.range.start, group.range.end)
|
|
431
|
+
: group.tokens.map((token) => token.original).join(" ");
|
|
432
|
+
return {
|
|
433
|
+
message: "Unrecognized text",
|
|
434
|
+
text: text.trim() || text,
|
|
435
|
+
tokens: group.tokens.map((token) => token.original),
|
|
436
|
+
range: group.range
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
return { result, issues };
|
|
440
|
+
}
|
package/dist/parser.js
CHANGED
|
@@ -1870,6 +1870,9 @@ function parseInternal(input, options) {
|
|
|
1870
1870
|
: maps_1.DEFAULT_ROUTE_SYNONYMS[phrase];
|
|
1871
1871
|
if (synonym) {
|
|
1872
1872
|
if (phrase === "in" && slice.length === 1) {
|
|
1873
|
+
if (internal.routeCode) {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1873
1876
|
const prevToken = tokens[startIndex - 1];
|
|
1874
1877
|
if (prevToken && !internal.consumed.has(prevToken.index)) {
|
|
1875
1878
|
continue;
|
package/dist/schedule.js
CHANGED
|
@@ -433,6 +433,25 @@ function mergeFrequencyDefaults(base, override) {
|
|
|
433
433
|
}
|
|
434
434
|
return merged;
|
|
435
435
|
}
|
|
436
|
+
function inferDailyFrequencyClocks(frequency) {
|
|
437
|
+
if (!Number.isFinite(frequency) || frequency <= 0) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
if (frequency === 1) {
|
|
441
|
+
return ["09:00:00"];
|
|
442
|
+
}
|
|
443
|
+
const startMinutes = 8 * 60;
|
|
444
|
+
const endMinutes = 20 * 60;
|
|
445
|
+
const spanMinutes = endMinutes - startMinutes;
|
|
446
|
+
const clocks = new Set();
|
|
447
|
+
for (let index = 0; index < frequency; index += 1) {
|
|
448
|
+
const minutes = startMinutes + Math.round((spanMinutes * index) / (frequency - 1));
|
|
449
|
+
const hour = Math.floor(minutes / SECONDS_PER_MINUTE);
|
|
450
|
+
const minute = minutes % SECONDS_PER_MINUTE;
|
|
451
|
+
clocks.add(`${pad(hour)}:${pad(minute)}:00`);
|
|
452
|
+
}
|
|
453
|
+
return Array.from(clocks).sort();
|
|
454
|
+
}
|
|
436
455
|
/** Resolves fallback clock arrays for frequency-only schedules. */
|
|
437
456
|
function resolveFrequencyClocks(timing, config) {
|
|
438
457
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
@@ -462,6 +481,11 @@ function resolveFrequencyClocks(timing, config) {
|
|
|
462
481
|
collected.add(normalizeClock(clock));
|
|
463
482
|
}
|
|
464
483
|
}
|
|
484
|
+
if (collected.size === 0 && repeat.period === 1 && repeat.periodUnit === "d") {
|
|
485
|
+
for (const clock of inferDailyFrequencyClocks(repeat.frequency)) {
|
|
486
|
+
collected.add(clock);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
465
489
|
}
|
|
466
490
|
return Array.from(collected).sort();
|
|
467
491
|
}
|
|
@@ -561,46 +585,53 @@ function nextDueDoses(dosage, options) {
|
|
|
561
585
|
if (results.length >= effectiveLimit) {
|
|
562
586
|
return results.slice(0, effectiveLimit);
|
|
563
587
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
588
|
+
const canFallbackToFrequency = expanded.length === 0 &&
|
|
589
|
+
timeOfDayEntries.length === 0 &&
|
|
590
|
+
!!repeat.frequency &&
|
|
591
|
+
!!repeat.period &&
|
|
592
|
+
!!repeat.periodUnit;
|
|
593
|
+
if (!canFallbackToFrequency) {
|
|
594
|
+
if (expanded.length === 0) {
|
|
595
|
+
return results.slice(0, effectiveLimit);
|
|
596
|
+
}
|
|
597
|
+
let currentDay = startOfLocalDay(from, timeZone);
|
|
598
|
+
let iterations = 0;
|
|
599
|
+
const maxIterations = effectiveLimit * 31;
|
|
600
|
+
while (results.length < effectiveLimit && iterations < maxIterations) {
|
|
601
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
602
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
603
|
+
for (const entry of expanded) {
|
|
604
|
+
const targetDay = entry.dayShift === 0
|
|
605
|
+
? currentDay
|
|
606
|
+
: addLocalDays(currentDay, entry.dayShift, timeZone);
|
|
607
|
+
const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
|
|
608
|
+
if (!zoned) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (zoned < from) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (orderedAt && zoned < orderedAt) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const iso = formatZonedIso(zoned, timeZone);
|
|
618
|
+
if (!seen.has(iso)) {
|
|
619
|
+
seen.add(iso);
|
|
620
|
+
results.push(iso);
|
|
621
|
+
if (results.length === effectiveLimit) {
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
593
624
|
}
|
|
594
625
|
}
|
|
595
626
|
}
|
|
627
|
+
if (results.length >= effectiveLimit) {
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
631
|
+
iterations += 1;
|
|
596
632
|
}
|
|
597
|
-
|
|
598
|
-
break;
|
|
599
|
-
}
|
|
600
|
-
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
601
|
-
iterations += 1;
|
|
633
|
+
return results.slice(0, effectiveLimit);
|
|
602
634
|
}
|
|
603
|
-
return results.slice(0, effectiveLimit);
|
|
604
635
|
}
|
|
605
636
|
const treatAsInterval = !!repeat.period &&
|
|
606
637
|
!!repeat.periodUnit &&
|
|
@@ -705,35 +736,42 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
|
|
|
705
736
|
return count;
|
|
706
737
|
}
|
|
707
738
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
739
|
+
const canFallbackToFrequency = expanded.length === 0 &&
|
|
740
|
+
timeOfDayEntries.length === 0 &&
|
|
741
|
+
!!repeat.frequency &&
|
|
742
|
+
!!repeat.period &&
|
|
743
|
+
!!repeat.periodUnit;
|
|
744
|
+
if (!canFallbackToFrequency) {
|
|
745
|
+
if (expanded.length === 0) {
|
|
746
|
+
return count;
|
|
747
|
+
}
|
|
748
|
+
let currentDay = startOfLocalDay(orderedAt, timeZone);
|
|
749
|
+
let iterations = 0;
|
|
750
|
+
const maxIterations = normalizedCount !== undefined ? normalizedCount * 31 : 31 * 365;
|
|
751
|
+
while (currentDay < from && iterations < maxIterations) {
|
|
752
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
753
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
754
|
+
for (const entry of expanded) {
|
|
755
|
+
const targetDay = entry.dayShift === 0
|
|
756
|
+
? currentDay
|
|
757
|
+
: addLocalDays(currentDay, entry.dayShift, timeZone);
|
|
758
|
+
const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
|
|
759
|
+
if (!zoned) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (zoned < orderedAt || zoned >= from) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (recordCandidate(zoned) && normalizedCount !== undefined && seen.size >= normalizedCount) {
|
|
766
|
+
return count;
|
|
767
|
+
}
|
|
730
768
|
}
|
|
731
769
|
}
|
|
770
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
771
|
+
iterations += 1;
|
|
732
772
|
}
|
|
733
|
-
|
|
734
|
-
iterations += 1;
|
|
773
|
+
return count;
|
|
735
774
|
}
|
|
736
|
-
return count;
|
|
737
775
|
}
|
|
738
776
|
const treatAsInterval = !!repeat.period &&
|
|
739
777
|
!!repeat.periodUnit &&
|
|
@@ -949,27 +987,34 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
|
|
|
949
987
|
recordCandidate(immediateSource);
|
|
950
988
|
}
|
|
951
989
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
990
|
+
const canFallbackToFrequency = expanded.length === 0 &&
|
|
991
|
+
timeOfDayEntries.length === 0 &&
|
|
992
|
+
!!repeat.frequency &&
|
|
993
|
+
!!repeat.period &&
|
|
994
|
+
!!repeat.periodUnit;
|
|
995
|
+
if (!canFallbackToFrequency) {
|
|
996
|
+
if (expanded.length === 0)
|
|
997
|
+
return count;
|
|
998
|
+
let currentDay = startOfLocalDay(from, timeZone);
|
|
999
|
+
let iterations = 0;
|
|
1000
|
+
const maxIterations = limit !== undefined ? limit * 31 : 365 * 31;
|
|
1001
|
+
while (count < (limit !== null && limit !== void 0 ? limit : Infinity) && currentDay < to && iterations < maxIterations) {
|
|
1002
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
1003
|
+
if (!enforceDayFilter || dayFilter.has(weekday)) {
|
|
1004
|
+
for (const entry of expanded) {
|
|
1005
|
+
const targetDay = entry.dayShift === 0
|
|
1006
|
+
? currentDay
|
|
1007
|
+
: addLocalDays(currentDay, entry.dayShift, timeZone);
|
|
1008
|
+
const zoned = makeZonedDateFromDay(targetDay, timeZone, entry.time);
|
|
1009
|
+
if (zoned)
|
|
1010
|
+
recordCandidate(zoned);
|
|
1011
|
+
}
|
|
967
1012
|
}
|
|
1013
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
1014
|
+
iterations += 1;
|
|
968
1015
|
}
|
|
969
|
-
|
|
970
|
-
iterations += 1;
|
|
1016
|
+
return count;
|
|
971
1017
|
}
|
|
972
|
-
return count;
|
|
973
1018
|
}
|
|
974
1019
|
const treatAsInterval = !!repeat.period &&
|
|
975
1020
|
!!repeat.periodUnit &&
|
|
@@ -1047,7 +1092,7 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
|
|
|
1047
1092
|
}
|
|
1048
1093
|
return count;
|
|
1049
1094
|
}
|
|
1050
|
-
function
|
|
1095
|
+
function calculateTotalUnitsSingle(options) {
|
|
1051
1096
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
1052
1097
|
const { dosage, durationValue, durationUnit, roundToMultiple, context } = options;
|
|
1053
1098
|
const from = coerceDate(options.from, "from");
|
|
@@ -1102,3 +1147,24 @@ function calculateTotalUnits(options) {
|
|
|
1102
1147
|
}
|
|
1103
1148
|
return result;
|
|
1104
1149
|
}
|
|
1150
|
+
function calculateTotalUnits(options) {
|
|
1151
|
+
if (Array.isArray(options.dosage)) {
|
|
1152
|
+
const hasAnyDosage = options.dosage.length > 0;
|
|
1153
|
+
if (!hasAnyDosage) {
|
|
1154
|
+
return { totalUnits: 0 };
|
|
1155
|
+
}
|
|
1156
|
+
let totalUnits = 0;
|
|
1157
|
+
let totalContainers = 0;
|
|
1158
|
+
let sawContainers = false;
|
|
1159
|
+
for (const dosage of options.dosage) {
|
|
1160
|
+
const result = calculateTotalUnitsSingle(Object.assign(Object.assign({}, options), { dosage }));
|
|
1161
|
+
totalUnits += result.totalUnits;
|
|
1162
|
+
if (result.totalContainers !== undefined) {
|
|
1163
|
+
totalContainers += result.totalContainers;
|
|
1164
|
+
sawContainers = true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return sawContainers ? { totalUnits, totalContainers } : { totalUnits };
|
|
1168
|
+
}
|
|
1169
|
+
return calculateTotalUnitsSingle(options);
|
|
1170
|
+
}
|
package/dist/segment.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitSigSegments = splitSigSegments;
|
|
4
|
+
const COMMA_SEGMENT_STARTERS = new Set([
|
|
5
|
+
"apply",
|
|
6
|
+
"take",
|
|
7
|
+
"instill",
|
|
8
|
+
"inject",
|
|
9
|
+
"spray",
|
|
10
|
+
"use",
|
|
11
|
+
"od",
|
|
12
|
+
"os",
|
|
13
|
+
"ou",
|
|
14
|
+
"re",
|
|
15
|
+
"le",
|
|
16
|
+
"be",
|
|
17
|
+
"right",
|
|
18
|
+
"left",
|
|
19
|
+
"both",
|
|
20
|
+
"each"
|
|
21
|
+
]);
|
|
22
|
+
const DIRECTIONAL_SEGMENT_STARTERS = new Set(["right", "left", "both", "each"]);
|
|
23
|
+
const SEPARATOR_RULES = [
|
|
24
|
+
{
|
|
25
|
+
name: "double-slash",
|
|
26
|
+
match: (input, index) => (input.startsWith("//", index) ? 2 : 0)
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "line-break",
|
|
30
|
+
match: (input, index) => {
|
|
31
|
+
const ch = input[index];
|
|
32
|
+
if (ch === "\r" && input[index + 1] === "\n") {
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
return ch === "\n" || ch === "\r" ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "pipe",
|
|
40
|
+
match: (input, index) => input[index] === "|" && hasNonWhitespaceAround(input, index) ? consumePipeRun(input, index) : 0
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "plus",
|
|
44
|
+
match: (input, index) => input[index] === "+" && hasNonWhitespaceAround(input, index) ? 1 : 0
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "slash-divider",
|
|
48
|
+
match: (input, index) => input[index] === "/" && isDividerSlash(input, index) ? 1 : 0
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "comma-clause",
|
|
52
|
+
match: (input, index, currentStart) => input[index] === "," && shouldSplitComma(input, index, currentStart) ? 1 : 0
|
|
53
|
+
}
|
|
54
|
+
];
|
|
55
|
+
function splitSigSegments(input) {
|
|
56
|
+
const segments = [];
|
|
57
|
+
let currentStart = 0;
|
|
58
|
+
let depth = 0;
|
|
59
|
+
const pushSegment = (rawStart, rawEnd) => {
|
|
60
|
+
let start = rawStart;
|
|
61
|
+
let end = rawEnd;
|
|
62
|
+
while (start < end && /\s/.test(input[start])) {
|
|
63
|
+
start += 1;
|
|
64
|
+
}
|
|
65
|
+
while (end > start && /\s/.test(input[end - 1])) {
|
|
66
|
+
end -= 1;
|
|
67
|
+
}
|
|
68
|
+
if (end <= start) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
segments.push({
|
|
72
|
+
text: input.slice(start, end),
|
|
73
|
+
start,
|
|
74
|
+
end
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
78
|
+
const ch = input[index];
|
|
79
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
80
|
+
depth += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if ((ch === ")" || ch === "]" || ch === "}") && depth > 0) {
|
|
84
|
+
depth -= 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (depth > 0) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const rule of SEPARATOR_RULES) {
|
|
91
|
+
const length = rule.match(input, index, currentStart);
|
|
92
|
+
if (!length) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
pushSegment(currentStart, index);
|
|
96
|
+
currentStart = index + length;
|
|
97
|
+
index = currentStart - 1;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
pushSegment(currentStart, input.length);
|
|
102
|
+
if (segments.length > 0) {
|
|
103
|
+
return segments;
|
|
104
|
+
}
|
|
105
|
+
const fallback = input.trim();
|
|
106
|
+
if (!fallback) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const start = input.indexOf(fallback);
|
|
110
|
+
return [{ text: fallback, start, end: start + fallback.length }];
|
|
111
|
+
}
|
|
112
|
+
function hasNonWhitespaceAround(input, index) {
|
|
113
|
+
const left = previousNonWhitespace(input, index - 1);
|
|
114
|
+
const right = nextNonWhitespace(input, index + 1);
|
|
115
|
+
return left !== undefined && right !== undefined;
|
|
116
|
+
}
|
|
117
|
+
function isDividerSlash(input, index) {
|
|
118
|
+
var _a, _b;
|
|
119
|
+
if (input[index - 1] === "/" || input[index + 1] === "/") {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const previous = previousNonWhitespace(input, index - 1);
|
|
123
|
+
const next = nextNonWhitespace(input, index + 1);
|
|
124
|
+
if (previous === undefined || next === undefined) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (/\d/.test(previous) && /\d/.test(next)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return /\s/.test((_a = input[index - 1]) !== null && _a !== void 0 ? _a : "") || /\s/.test((_b = input[index + 1]) !== null && _b !== void 0 ? _b : "");
|
|
131
|
+
}
|
|
132
|
+
function shouldSplitComma(input, index, currentStart) {
|
|
133
|
+
var _a, _b;
|
|
134
|
+
if (!hasNonWhitespaceAround(input, index)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const left = input.slice(currentStart, index).trim();
|
|
138
|
+
const right = input.slice(index + 1).trim();
|
|
139
|
+
if (!left || !right) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (startsWithTimeExpression(right)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const rightToken = (_b = (_a = right.match(/^([a-z]+|\d+(?:\.\d+)?)/i)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
146
|
+
if (!rightToken) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (/^\d/.test(rightToken)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (COMMA_SEGMENT_STARTERS.has(rightToken)) {
|
|
153
|
+
if (DIRECTIONAL_SEGMENT_STARTERS.has(rightToken)) {
|
|
154
|
+
return looksLikeDirectionalClause(right);
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function looksLikeDirectionalClause(text) {
|
|
161
|
+
const normalized = text.trim().toLowerCase();
|
|
162
|
+
if (!normalized) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (/\b\d+(?:\.\d+)?\b/.test(normalized)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return /\b(once|twice|thrice|daily|bid|tid|qid|q\d+[a-z0-9/-]*|every|prn|hs|morning|lunch|dinner|noon|night|weekly|monthly)\b/.test(normalized);
|
|
169
|
+
}
|
|
170
|
+
function startsWithTimeExpression(text) {
|
|
171
|
+
const trimmed = text.replace(/^\s+/, "");
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return (/^@\s*\d{1,2}([:.]\d{2})?\s*(am|pm)?\b/i.test(trimmed) ||
|
|
176
|
+
/^\d{1,2}[:.]\d{2}\s*(am|pm)?\b/i.test(trimmed) ||
|
|
177
|
+
/^\d{1,2}\s*(am|pm)\b/i.test(trimmed));
|
|
178
|
+
}
|
|
179
|
+
function previousNonWhitespace(input, index) {
|
|
180
|
+
for (let cursor = index; cursor >= 0; cursor -= 1) {
|
|
181
|
+
const ch = input[cursor];
|
|
182
|
+
if (!/\s/.test(ch)) {
|
|
183
|
+
return ch;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
function nextNonWhitespace(input, index) {
|
|
189
|
+
for (let cursor = index; cursor < input.length; cursor += 1) {
|
|
190
|
+
const ch = input[cursor];
|
|
191
|
+
if (!/\s/.test(ch)) {
|
|
192
|
+
return ch;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
function consumePipeRun(input, index) {
|
|
198
|
+
let cursor = index;
|
|
199
|
+
while (cursor < input.length && input[cursor] === "|") {
|
|
200
|
+
cursor += 1;
|
|
201
|
+
}
|
|
202
|
+
return cursor - index;
|
|
203
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -297,6 +297,13 @@ export interface FormatOptions {
|
|
|
297
297
|
locale?: "en" | "th" | string;
|
|
298
298
|
i18n?: SigTranslationConfig;
|
|
299
299
|
}
|
|
300
|
+
export interface FormatBatchOptions extends FormatOptions {
|
|
301
|
+
/**
|
|
302
|
+
* String inserted between formatted clauses. Defaults to ", " so output can
|
|
303
|
+
* be fed back into `parseSig` as a multi-clause instruction.
|
|
304
|
+
*/
|
|
305
|
+
separator?: string;
|
|
306
|
+
}
|
|
300
307
|
export interface BodySiteCode {
|
|
301
308
|
code: string;
|
|
302
309
|
display?: string;
|
|
@@ -535,6 +542,61 @@ export interface ParseResult {
|
|
|
535
542
|
}>;
|
|
536
543
|
};
|
|
537
544
|
}
|
|
545
|
+
export interface ParseBatchSegmentMeta {
|
|
546
|
+
index: number;
|
|
547
|
+
text: string;
|
|
548
|
+
range: TextRange;
|
|
549
|
+
}
|
|
550
|
+
export interface ParseBatchResult {
|
|
551
|
+
input: string;
|
|
552
|
+
count: number;
|
|
553
|
+
items: ParseResult[];
|
|
554
|
+
/**
|
|
555
|
+
* Legacy compatibility field mirroring the first parsed item so existing
|
|
556
|
+
* single-sig integrations can migrate incrementally.
|
|
557
|
+
*/
|
|
558
|
+
fhir: FhirDosage;
|
|
559
|
+
/**
|
|
560
|
+
* Legacy compatibility field mirroring the first parsed item so existing
|
|
561
|
+
* single-sig integrations can migrate incrementally.
|
|
562
|
+
*/
|
|
563
|
+
shortText: string;
|
|
564
|
+
/**
|
|
565
|
+
* Legacy compatibility field mirroring the first parsed item so existing
|
|
566
|
+
* single-sig integrations can migrate incrementally.
|
|
567
|
+
*/
|
|
568
|
+
longText: string;
|
|
569
|
+
warnings: string[];
|
|
570
|
+
meta: {
|
|
571
|
+
consumedTokens: string[];
|
|
572
|
+
leftoverText?: string;
|
|
573
|
+
normalized: {
|
|
574
|
+
route?: RouteCode;
|
|
575
|
+
unit?: string;
|
|
576
|
+
site?: {
|
|
577
|
+
text?: string;
|
|
578
|
+
coding?: BodySiteCode;
|
|
579
|
+
};
|
|
580
|
+
prnReason?: {
|
|
581
|
+
text?: string;
|
|
582
|
+
coding?: FhirCoding;
|
|
583
|
+
};
|
|
584
|
+
additionalInstructions?: Array<{
|
|
585
|
+
text?: string;
|
|
586
|
+
coding?: FhirCoding;
|
|
587
|
+
}>;
|
|
588
|
+
};
|
|
589
|
+
siteLookups?: Array<{
|
|
590
|
+
request: SiteCodeLookupRequest;
|
|
591
|
+
suggestions: SiteCodeSuggestion[];
|
|
592
|
+
}>;
|
|
593
|
+
prnReasonLookups?: Array<{
|
|
594
|
+
request: PrnReasonLookupRequest;
|
|
595
|
+
suggestions: PrnReasonSuggestion[];
|
|
596
|
+
}>;
|
|
597
|
+
segments: ParseBatchSegmentMeta[];
|
|
598
|
+
};
|
|
599
|
+
}
|
|
538
600
|
export interface LintIssue {
|
|
539
601
|
/** Human-readable description of why the segment could not be parsed. */
|
|
540
602
|
message: string;
|
|
@@ -551,6 +613,20 @@ export interface LintResult {
|
|
|
551
613
|
/** Segments of the input that could not be interpreted. */
|
|
552
614
|
issues: LintIssue[];
|
|
553
615
|
}
|
|
616
|
+
export interface LintBatchResult {
|
|
617
|
+
input: string;
|
|
618
|
+
count: number;
|
|
619
|
+
items: LintResult[];
|
|
620
|
+
/**
|
|
621
|
+
* Legacy compatibility fields mirroring the first parsed item so existing
|
|
622
|
+
* consumers of `lintSig` can migrate incrementally.
|
|
623
|
+
*/
|
|
624
|
+
result: ParseResult;
|
|
625
|
+
issues: LintIssue[];
|
|
626
|
+
meta: {
|
|
627
|
+
segments: ParseBatchSegmentMeta[];
|
|
628
|
+
};
|
|
629
|
+
}
|
|
554
630
|
/**
|
|
555
631
|
* Maps EventTiming codes (or other institution-specific timing strings) to
|
|
556
632
|
* 24-hour clock representations such as "08:00".
|
|
@@ -597,7 +673,7 @@ export interface TotalUnitsResult {
|
|
|
597
673
|
totalContainers?: number;
|
|
598
674
|
}
|
|
599
675
|
export interface TotalUnitsOptions extends NextDueDoseOptions {
|
|
600
|
-
dosage: FhirDosage;
|
|
676
|
+
dosage: FhirDosage | FhirDosage[];
|
|
601
677
|
durationValue: number;
|
|
602
678
|
durationUnit: FhirPeriodUnit;
|
|
603
679
|
roundToMultiple?: number;
|