ezmedicationinput 0.1.33 → 0.1.35
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/maps.js +4 -0
- package/dist/parser.js +3 -0
- package/dist/segment.d.ts +6 -0
- package/dist/segment.js +203 -0
- package/dist/types.d.ts +76 -0
- 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/maps.js
CHANGED
|
@@ -145,6 +145,10 @@ exports.DEFAULT_ROUTE_SYNONYMS = (() => {
|
|
|
145
145
|
registerVariants("transdermal", types_1.RouteCode["Transdermal route"]);
|
|
146
146
|
registerVariants("pr", types_1.RouteCode["Per rectum"]);
|
|
147
147
|
registerVariants("rectal", types_1.RouteCode["Per rectum"]);
|
|
148
|
+
registerVariants("supp", types_1.RouteCode["Per rectum"]);
|
|
149
|
+
registerVariants("suppo", types_1.RouteCode["Per rectum"]);
|
|
150
|
+
registerVariants("suppository", types_1.RouteCode["Per rectum"]);
|
|
151
|
+
registerVariants("suppositories", types_1.RouteCode["Per rectum"]);
|
|
148
152
|
registerVariants("pv", types_1.RouteCode["Per vagina"]);
|
|
149
153
|
registerVariants("vaginal", types_1.RouteCode["Per vagina"]);
|
|
150
154
|
registerVariants("oph", types_1.RouteCode["Ophthalmic route"]);
|
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/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".
|