ezmedicationinput 0.1.34 → 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 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 result = parseSig("1x3 po pc", { context: { dosageForm: "tab" } });
32
- console.log(result.fhir);
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
- "text": "1 tablet by mouth three times daily after meals",
40
- "timing": {
41
- "code": { "coding": [{ "code": "TID" }], "text": "TID" },
42
- "repeat": {
43
- "frequency": 3,
44
- "period": 1,
45
- "periodUnit": "d",
46
- "when": ["PC"]
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
- "route": { "text": "by mouth" },
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
- `ParseResult.meta.prnReasonLookups` so client applications can prompt the user
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, LintResult, ParseOptions, ParseResult } from "./types";
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): ParseResult;
11
- export declare function lintSig(input: string, options?: ParseOptions): LintResult;
12
- export declare function parseSigAsync(input: string, options?: ParseOptions): Promise<ParseResult>;
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 internal = (0, parser_1.parseInternal)(input, options);
60
- (0, parser_1.applyPrnReasonCoding)(internal, options);
61
- (0, parser_1.applySiteCoding)(internal, options);
62
- return buildParseResult(internal, options);
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 internal = (0, parser_1.parseInternal)(input, options);
66
- (0, parser_1.applyPrnReasonCoding)(internal, options);
67
- (0, parser_1.applySiteCoding)(internal, options);
68
- const result = buildParseResult(internal, options);
69
- const groups = (0, parser_1.findUnparsedTokenGroups)(internal);
70
- const issues = groups.map((group) => {
71
- const text = group.range
72
- ? internal.input.slice(group.range.start, group.range.end)
73
- : group.tokens.map((token) => token.original).join(" ");
74
- return {
75
- message: "Unrecognized text",
76
- text: text.trim() || text,
77
- tokens: group.tokens.map((token) => token.original),
78
- range: group.range
79
- };
80
- });
81
- return { result, issues };
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 internal = (0, parser_1.parseInternal)(input, options);
86
- yield (0, parser_1.applyPrnReasonCodingAsync)(internal, options);
87
- yield (0, parser_1.applySiteCodingAsync)(internal, options);
88
- return buildParseResult(internal, options);
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;
@@ -0,0 +1,6 @@
1
+ export interface SigSegment {
2
+ text: string;
3
+ start: number;
4
+ end: number;
5
+ }
6
+ export declare function splitSigSegments(input: string): SigSegment[];
@@ -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".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",