ezmedicationinput 0.1.15 → 0.1.17

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
@@ -8,6 +8,7 @@
8
8
  - Emits timing abbreviations (`timing.code`) and repeat structures simultaneously where possible.
9
9
  - Maps meal/time blocks to the correct `Timing.repeat.when` **EventTiming** codes and can auto-expand AC/PC/C into specific meals.
10
10
  - Outputs SNOMED CT route codings (while providing friendly text) and round-trips known SNOMED routes back into the parser.
11
+ - Auto-codes common PRN (as-needed) reasons and additional dosage instructions while keeping the raw text when no coding is available.
11
12
  - Understands ocular and intravitreal shorthand (OD/OS/OU, LE/RE/BE, IVT*, VOD/VOS, etc.) and warns when intravitreal instructions omit an eye side.
12
13
  - Parses fractional/ minute-based intervals (`q0.5h`, `q30 min`, `q1/4hr`) plus dose and timing ranges.
13
14
  - Supports extensible dictionaries for routes, units, frequency shorthands, and event timing tokens.
@@ -50,6 +51,59 @@ Example output:
50
51
  }
51
52
  ```
52
53
 
54
+ ### PRN reasons & additional instructions
55
+
56
+ `parseSig` identifies PRN (as-needed) clauses and trailing instructions, then
57
+ codes them with SNOMED CT whenever possible.
58
+
59
+ ```ts
60
+ const result = parseSig("1 tab po q4h prn headache; do not exceed 6 tabs/day");
61
+
62
+ result.fhir.asNeededFor;
63
+ // → [{
64
+ // text: "headache",
65
+ // coding: [{
66
+ // system: "http://snomed.info/sct",
67
+ // code: "25064002",
68
+ // display: "Headache"
69
+ // }]
70
+ // }]
71
+
72
+ result.fhir.additionalInstruction;
73
+ // → [{ text: "Do not exceed 6 tablets daily" }]
74
+ ```
75
+
76
+ Customize the dictionaries and lookups through `ParseOptions`:
77
+
78
+ ```ts
79
+ parseSig(input, {
80
+ prnReasonMap: {
81
+ migraine: {
82
+ text: "Migraine",
83
+ coding: {
84
+ system: "http://snomed.info/sct",
85
+ code: "37796009",
86
+ display: "Migraine"
87
+ }
88
+ }
89
+ },
90
+ prnReasonResolvers: async (request) => terminologyService.lookup(request),
91
+ prnReasonSuggestionResolvers: async (request) => terminologyService.suggest(request),
92
+ });
93
+ ```
94
+
95
+ Use `{reason}` in the sig string (e.g. `prn {migraine}`) to force a lookup even
96
+ when a direct match exists. Additional instructions are sourced from a built-in
97
+ set of SNOMED CT concepts under *419492006 – Additional dosage instructions* and
98
+ fall back to plain text when no coding is available. Parsed instructions are
99
+ also echoed in `ParseResult.meta.normalized.additionalInstructions` for quick UI
100
+ rendering.
101
+
102
+ When a PRN reason cannot be auto-resolved, any registered suggestion resolvers
103
+ are invoked and their responses are surfaced through
104
+ `ParseResult.meta.prnReasonLookups` so client applications can prompt the user
105
+ to choose a coded concept.
106
+
53
107
  ### Sig (directions) suggestions
54
108
 
55
109
  Use `suggestSig` to drive autocomplete experiences while the clinician is
@@ -126,6 +180,8 @@ result.fhir.site?.coding?.[0];
126
180
 
127
181
  When the parser encounters an unfamiliar site, it leaves the text untouched and records nothing in `meta.siteLookups`. Wrapping the phrase in braces (e.g. `apply to {mole on scalp}`) preserves the same parsing behavior but flags the entry as a **probe** so `meta.siteLookups` always contains the request. This allows UIs to display lookup widgets even before a matching code exists. Braces are optional when the site is already recognized—they simply make the clinician's intent explicit.
128
182
 
183
+ Unknown body sites still populate `Dosage.site.text` and `ParseResult.meta.normalized.site.text`, allowing UIs to echo the verbatim phrase while terminology lookups run asynchronously.
184
+
129
185
  You can extend or replace the built-in codings via `ParseOptions`:
130
186
 
131
187
  ```ts
package/dist/fhir.js CHANGED
@@ -9,7 +9,7 @@ const object_1 = require("./utils/object");
9
9
  const array_1 = require("./utils/array");
10
10
  const SNOMED_SYSTEM = "http://snomed.info/sct";
11
11
  function toFhir(internal) {
12
- var _a, _b, _c, _d, _e;
12
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
13
13
  const dosage = {};
14
14
  const repeat = {};
15
15
  let hasRepeat = false;
@@ -115,10 +115,40 @@ function toFhir(internal) {
115
115
  coding
116
116
  };
117
117
  }
118
+ if ((_f = internal.additionalInstructions) === null || _f === void 0 ? void 0 : _f.length) {
119
+ dosage.additionalInstruction = internal.additionalInstructions.map((instruction) => {
120
+ var _a, _b;
121
+ return ({
122
+ text: instruction.text,
123
+ coding: ((_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.code)
124
+ ? [
125
+ {
126
+ system: (_b = instruction.coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM,
127
+ code: instruction.coding.code,
128
+ display: instruction.coding.display
129
+ }
130
+ ]
131
+ : undefined
132
+ });
133
+ });
134
+ }
118
135
  if (internal.asNeeded) {
119
136
  dosage.asNeededBoolean = true;
120
- if (internal.asNeededReason) {
121
- dosage.asNeededFor = [{ text: internal.asNeededReason }];
137
+ if (internal.asNeededReason || ((_g = internal.asNeededReasonCoding) === null || _g === void 0 ? void 0 : _g.code)) {
138
+ const concept = {};
139
+ if (internal.asNeededReason) {
140
+ concept.text = internal.asNeededReason;
141
+ }
142
+ if ((_h = internal.asNeededReasonCoding) === null || _h === void 0 ? void 0 : _h.code) {
143
+ concept.coding = [
144
+ {
145
+ system: (_j = internal.asNeededReasonCoding.system) !== null && _j !== void 0 ? _j : SNOMED_SYSTEM,
146
+ code: internal.asNeededReasonCoding.code,
147
+ display: internal.asNeededReasonCoding.display
148
+ }
149
+ ];
150
+ }
151
+ dosage.asNeededFor = [concept];
122
152
  }
123
153
  }
124
154
  const longText = (0, format_1.formatInternal)(internal, "long");
@@ -128,7 +158,7 @@ function toFhir(internal) {
128
158
  return dosage;
129
159
  }
130
160
  function internalFromFhir(dosage) {
131
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7;
161
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11;
132
162
  const internal = {
133
163
  input: (_a = dosage.text) !== null && _a !== void 0 ? _a : "",
134
164
  tokens: [],
@@ -152,7 +182,9 @@ function internalFromFhir(dosage) {
152
182
  asNeeded: dosage.asNeededBoolean,
153
183
  asNeededReason: (_0 = (_z = dosage.asNeededFor) === null || _z === void 0 ? void 0 : _z[0]) === null || _0 === void 0 ? void 0 : _0.text,
154
184
  siteTokenIndices: new Set(),
155
- siteLookups: []
185
+ siteLookups: [],
186
+ prnReasonLookups: [],
187
+ additionalInstructions: []
156
188
  };
157
189
  const routeCoding = (_2 = (_1 = dosage.route) === null || _1 === void 0 ? void 0 : _1.coding) === null || _2 === void 0 ? void 0 : _2.find((code) => code.system === SNOMED_SYSTEM);
158
190
  if (routeCoding === null || routeCoding === void 0 ? void 0 : routeCoding.code) {
@@ -171,13 +203,36 @@ function internalFromFhir(dosage) {
171
203
  system: siteCoding.system
172
204
  };
173
205
  }
174
- const doseAndRate = (_5 = dosage.doseAndRate) === null || _5 === void 0 ? void 0 : _5[0];
206
+ const reasonCoding = (_7 = (_6 = (_5 = dosage.asNeededFor) === null || _5 === void 0 ? void 0 : _5[0]) === null || _6 === void 0 ? void 0 : _6.coding) === null || _7 === void 0 ? void 0 : _7[0];
207
+ if (reasonCoding === null || reasonCoding === void 0 ? void 0 : reasonCoding.code) {
208
+ internal.asNeededReasonCoding = {
209
+ code: reasonCoding.code,
210
+ display: reasonCoding.display,
211
+ system: reasonCoding.system
212
+ };
213
+ }
214
+ if ((_8 = dosage.additionalInstruction) === null || _8 === void 0 ? void 0 : _8.length) {
215
+ internal.additionalInstructions = dosage.additionalInstruction.map((concept) => {
216
+ var _a;
217
+ return ({
218
+ text: concept.text,
219
+ coding: ((_a = concept.coding) === null || _a === void 0 ? void 0 : _a[0])
220
+ ? {
221
+ code: concept.coding[0].code,
222
+ display: concept.coding[0].display,
223
+ system: concept.coding[0].system
224
+ }
225
+ : undefined
226
+ });
227
+ });
228
+ }
229
+ const doseAndRate = (_9 = dosage.doseAndRate) === null || _9 === void 0 ? void 0 : _9[0];
175
230
  if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseRange) {
176
231
  const { low, high } = doseAndRate.doseRange;
177
232
  if ((low === null || low === void 0 ? void 0 : low.value) !== undefined && (high === null || high === void 0 ? void 0 : high.value) !== undefined) {
178
233
  internal.doseRange = { low: low.value, high: high.value };
179
234
  }
180
- internal.unit = (_7 = (_6 = low === null || low === void 0 ? void 0 : low.unit) !== null && _6 !== void 0 ? _6 : high === null || high === void 0 ? void 0 : high.unit) !== null && _7 !== void 0 ? _7 : internal.unit;
235
+ internal.unit = (_11 = (_10 = low === null || low === void 0 ? void 0 : low.unit) !== null && _10 !== void 0 ? _10 : high === null || high === void 0 ? void 0 : high.unit) !== null && _11 !== void 0 ? _11 : internal.unit;
181
236
  }
182
237
  else if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseQuantity) {
183
238
  const dose = doseAndRate.doseQuantity;
package/dist/format.js CHANGED
@@ -69,6 +69,11 @@ const ROUTE_GRAMMAR = {
69
69
  routePhrase: ({ hasSite }) => (hasSite ? undefined : "into the eye"),
70
70
  sitePreposition: "into"
71
71
  },
72
+ [types_1.RouteCode["Per rectum"]]: {
73
+ verb: "Use",
74
+ routePhrase: ({ hasSite }) => (hasSite ? undefined : "rectally"),
75
+ sitePreposition: "into"
76
+ },
72
77
  [types_1.RouteCode["Topical route"]]: {
73
78
  verb: "Apply",
74
79
  routePhrase: ({ hasSite }) => (hasSite ? undefined : "topically"),
@@ -137,6 +142,9 @@ function grammarFromRouteText(text) {
137
142
  if (normalized.includes("intravenous") || normalized === "iv") {
138
143
  return ROUTE_GRAMMAR[types_1.RouteCode["Intravenous route"]];
139
144
  }
145
+ if (normalized.includes("rectal") || normalized.includes("rectum")) {
146
+ return ROUTE_GRAMMAR[types_1.RouteCode["Per rectum"]];
147
+ }
140
148
  if (normalized.includes("nasal")) {
141
149
  return ROUTE_GRAMMAR[types_1.RouteCode["Nasal route"]];
142
150
  }
@@ -412,6 +420,11 @@ function formatSite(internal, grammar) {
412
420
  return undefined;
413
421
  }
414
422
  const lower = text.toLowerCase();
423
+ if (internal.routeCode === types_1.RouteCode["Per rectum"]) {
424
+ if (lower === "rectum" || lower === "rectal") {
425
+ return undefined;
426
+ }
427
+ }
415
428
  let preposition = grammar.sitePreposition;
416
429
  if (!preposition) {
417
430
  if (lower.includes("eye")) {
@@ -603,9 +616,33 @@ function formatLong(internal) {
603
616
  }
604
617
  const body = segments.filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
605
618
  if (!body) {
606
- return `${grammar.verb}.`;
619
+ const instructionText = formatAdditionalInstructions(internal);
620
+ if (!instructionText) {
621
+ return `${grammar.verb}.`;
622
+ }
623
+ return `${grammar.verb}. ${instructionText}`.trim();
624
+ }
625
+ const instructionText = formatAdditionalInstructions(internal);
626
+ const baseSentence = `${grammar.verb} ${body}.`;
627
+ return instructionText ? `${baseSentence} ${instructionText}` : baseSentence;
628
+ }
629
+ function formatAdditionalInstructions(internal) {
630
+ var _a;
631
+ if (!((_a = internal.additionalInstructions) === null || _a === void 0 ? void 0 : _a.length)) {
632
+ return undefined;
633
+ }
634
+ const phrases = internal.additionalInstructions
635
+ .map((instruction) => { var _a; return instruction.text || ((_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.display); })
636
+ .filter((text) => Boolean(text))
637
+ .map((text) => text.trim())
638
+ .filter((text) => text.length > 0);
639
+ if (!phrases.length) {
640
+ return undefined;
607
641
  }
608
- return `${grammar.verb} ${body}.`;
642
+ return phrases
643
+ .map((phrase) => (/[.!?]$/.test(phrase) ? phrase : `${phrase}.`))
644
+ .join(" ")
645
+ .trim();
609
646
  }
610
647
  function stripTrailingZero(value) {
611
648
  const text = value.toString();
package/dist/index.js CHANGED
@@ -46,12 +46,14 @@ Object.defineProperty(exports, "resolveSigLocalization", { enumerable: true, get
46
46
  Object.defineProperty(exports, "resolveSigTranslation", { enumerable: true, get: function () { return i18n_2.resolveSigTranslation; } });
47
47
  function parseSig(input, options) {
48
48
  const internal = (0, parser_1.parseInternal)(input, options);
49
+ (0, parser_1.applyPrnReasonCoding)(internal, options);
49
50
  (0, parser_1.applySiteCoding)(internal, options);
50
51
  return buildParseResult(internal, options);
51
52
  }
52
53
  function parseSigAsync(input, options) {
53
54
  return __awaiter(this, void 0, void 0, function* () {
54
55
  const internal = (0, parser_1.parseInternal)(input, options);
56
+ yield (0, parser_1.applyPrnReasonCodingAsync)(internal, options);
55
57
  yield (0, parser_1.applySiteCodingAsync)(internal, options);
56
58
  return buildParseResult(internal, options);
57
59
  });
@@ -62,7 +64,7 @@ function formatSig(dosage, style = "short", options) {
62
64
  return (0, format_1.formatInternal)(internal, style, localization);
63
65
  }
64
66
  function fromFhirDosage(dosage, options) {
65
- var _a, _b, _c;
67
+ var _a, _b, _c, _d, _e, _f;
66
68
  const internal = (0, fhir_1.internalFromFhir)(dosage);
67
69
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
68
70
  const shortText = (0, format_1.formatInternal)(internal, "short", localization);
@@ -89,13 +91,40 @@ function fromFhirDosage(dosage, options) {
89
91
  }
90
92
  : undefined
91
93
  }
94
+ : undefined,
95
+ prnReason: internal.asNeededReason || ((_d = internal.asNeededReasonCoding) === null || _d === void 0 ? void 0 : _d.code)
96
+ ? {
97
+ text: internal.asNeededReason,
98
+ coding: ((_e = internal.asNeededReasonCoding) === null || _e === void 0 ? void 0 : _e.code)
99
+ ? {
100
+ code: internal.asNeededReasonCoding.code,
101
+ display: internal.asNeededReasonCoding.display,
102
+ system: internal.asNeededReasonCoding.system
103
+ }
104
+ : undefined
105
+ }
106
+ : undefined,
107
+ additionalInstructions: ((_f = internal.additionalInstructions) === null || _f === void 0 ? void 0 : _f.length)
108
+ ? internal.additionalInstructions.map((instruction) => {
109
+ var _a;
110
+ return ({
111
+ text: instruction.text,
112
+ coding: ((_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.code)
113
+ ? {
114
+ code: instruction.coding.code,
115
+ display: instruction.coding.display,
116
+ system: instruction.coding.system
117
+ }
118
+ : undefined
119
+ });
120
+ })
92
121
  : undefined
93
122
  }
94
123
  }
95
124
  };
96
125
  }
97
126
  function buildParseResult(internal, options) {
98
- var _a;
127
+ var _a, _b, _c;
99
128
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
100
129
  const shortText = (0, format_1.formatInternal)(internal, "short", localization);
101
130
  const longText = (0, format_1.formatInternal)(internal, "long", localization);
@@ -114,6 +143,28 @@ function buildParseResult(internal, options) {
114
143
  system: internal.siteCoding.system
115
144
  }
116
145
  : undefined;
146
+ const prnReasonCoding = ((_b = internal.asNeededReasonCoding) === null || _b === void 0 ? void 0 : _b.code)
147
+ ? {
148
+ code: internal.asNeededReasonCoding.code,
149
+ display: internal.asNeededReasonCoding.display,
150
+ system: internal.asNeededReasonCoding.system
151
+ }
152
+ : undefined;
153
+ const additionalInstructions = ((_c = internal.additionalInstructions) === null || _c === void 0 ? void 0 : _c.length)
154
+ ? internal.additionalInstructions.map((instruction) => {
155
+ var _a;
156
+ return ({
157
+ text: instruction.text,
158
+ coding: ((_a = instruction.coding) === null || _a === void 0 ? void 0 : _a.code)
159
+ ? {
160
+ code: instruction.coding.code,
161
+ display: instruction.coding.display,
162
+ system: instruction.coding.system
163
+ }
164
+ : undefined
165
+ });
166
+ })
167
+ : undefined;
117
168
  const siteLookups = internal.siteLookups.length
118
169
  ? internal.siteLookups.map((entry) => ({
119
170
  request: entry.request,
@@ -127,6 +178,21 @@ function buildParseResult(internal, options) {
127
178
  }))
128
179
  }))
129
180
  : undefined;
181
+ const prnReasonLookups = internal.prnReasonLookups.length
182
+ ? internal.prnReasonLookups.map((entry) => ({
183
+ request: entry.request,
184
+ suggestions: entry.suggestions.map((suggestion) => ({
185
+ coding: suggestion.coding
186
+ ? {
187
+ code: suggestion.coding.code,
188
+ display: suggestion.coding.display,
189
+ system: suggestion.coding.system
190
+ }
191
+ : undefined,
192
+ text: suggestion.text
193
+ }))
194
+ }))
195
+ : undefined;
130
196
  return {
131
197
  fhir,
132
198
  shortText,
@@ -145,9 +211,17 @@ function buildParseResult(internal, options) {
145
211
  text: internal.siteText,
146
212
  coding: siteCoding
147
213
  }
148
- : undefined
214
+ : undefined,
215
+ prnReason: internal.asNeededReason || prnReasonCoding
216
+ ? {
217
+ text: internal.asNeededReason,
218
+ coding: prnReasonCoding
219
+ }
220
+ : undefined,
221
+ additionalInstructions
149
222
  },
150
- siteLookups
223
+ siteLookups,
224
+ prnReasonLookups
151
225
  }
152
226
  };
153
227
  }
@@ -1,8 +1,12 @@
1
- import { EventTiming, FhirCoding, FhirDayOfWeek, FhirPeriodUnit, RouteCode, SiteCodeLookupRequest, SiteCodeSuggestion } from "./types";
1
+ import { EventTiming, FhirCoding, FhirDayOfWeek, FhirPeriodUnit, PrnReasonLookupRequest, PrnReasonSuggestion, RouteCode, SiteCodeLookupRequest, SiteCodeSuggestion } from "./types";
2
2
  export interface SiteLookupDetail {
3
3
  request: SiteCodeLookupRequest;
4
4
  suggestions: SiteCodeSuggestion[];
5
5
  }
6
+ export interface PrnReasonLookupDetail {
7
+ request: PrnReasonLookupRequest;
8
+ suggestions: PrnReasonSuggestion[];
9
+ }
6
10
  export interface Token {
7
11
  original: string;
8
12
  lower: string;
@@ -31,6 +35,7 @@ export interface ParsedSigInternal {
31
35
  timingCode?: string;
32
36
  asNeeded?: boolean;
33
37
  asNeededReason?: string;
38
+ asNeededReasonCoding?: FhirCoding;
34
39
  warnings: string[];
35
40
  siteText?: string;
36
41
  siteSource?: "abbreviation" | "text";
@@ -39,4 +44,10 @@ export interface ParsedSigInternal {
39
44
  siteLookupRequest?: SiteCodeLookupRequest;
40
45
  siteLookups: SiteLookupDetail[];
41
46
  customSiteHints?: Set<string>;
47
+ prnReasonLookupRequest?: PrnReasonLookupRequest;
48
+ prnReasonLookups: PrnReasonLookupDetail[];
49
+ additionalInstructions: Array<{
50
+ text?: string;
51
+ coding?: FhirCoding;
52
+ }>;
42
53
  }
package/dist/maps.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BodySiteDefinition, EventTiming, FhirDayOfWeek, FhirPeriodUnit, RouteCode, SNOMEDCTRouteCodes } from "./types";
1
+ import { AdditionalInstructionDefinition, BodySiteDefinition, EventTiming, FhirDayOfWeek, FhirPeriodUnit, PrnReasonDefinition, RouteCode, SNOMEDCTRouteCodes } from "./types";
2
2
  /**
3
3
  * SNOMED CT codings aligned with every known RouteCode. Keeping the structure
4
4
  * data-driven ensures any additions to the enumeration are surfaced
@@ -58,3 +58,19 @@ export declare const KNOWN_DOSAGE_FORMS_TO_DOSE: Record<string, string>;
58
58
  export declare const KNOWN_TMT_DOSAGE_FORM_TO_SNOMED_ROUTE: Record<string, SNOMEDCTRouteCodes>;
59
59
  export declare const DEFAULT_UNIT_BY_NORMALIZED_FORM: Record<string, string>;
60
60
  export declare const DEFAULT_UNIT_BY_ROUTE: Partial<Record<RouteCode, string>>;
61
+ export declare function normalizePrnReasonKey(value: string): string;
62
+ export declare function normalizeAdditionalInstructionKey(value: string): string;
63
+ export interface PrnReasonDictionaryEntry {
64
+ canonical: string;
65
+ definition: PrnReasonDefinition;
66
+ terms: string[];
67
+ }
68
+ export declare const DEFAULT_PRN_REASON_ENTRIES: PrnReasonDictionaryEntry[];
69
+ export declare const DEFAULT_PRN_REASON_DEFINITIONS: Record<string, PrnReasonDefinition>;
70
+ export interface AdditionalInstructionDictionaryEntry {
71
+ canonical: string;
72
+ definition: AdditionalInstructionDefinition;
73
+ terms: string[];
74
+ }
75
+ export declare const DEFAULT_ADDITIONAL_INSTRUCTION_ENTRIES: AdditionalInstructionDictionaryEntry[];
76
+ export declare const DEFAULT_ADDITIONAL_INSTRUCTION_DEFINITIONS: Record<string, AdditionalInstructionDefinition>;