ezmedicationinput 0.1.0 → 0.1.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { FhirDosage, ParseOptions, ParseResult } from "./types";
2
2
  export { parseInternal } from "./parser";
3
+ export { suggestSig } from "./suggest";
3
4
  export * from "./types";
4
5
  export { nextDueDoses } from "./schedule";
5
6
  export declare function parseSig(input: string, options?: ParseOptions): ParseResult;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { formatInternal } from "./format";
2
2
  import { internalFromFhir, toFhir } from "./fhir";
3
3
  import { parseInternal } from "./parser";
4
4
  export { parseInternal } from "./parser";
5
+ export { suggestSig } from "./suggest";
5
6
  export * from "./types";
6
7
  export { nextDueDoses } from "./schedule";
7
8
  export function parseSig(input, options) {
package/dist/maps.js CHANGED
@@ -53,63 +53,83 @@ export const DEFAULT_ROUTE_SYNONYMS = (() => {
53
53
  }
54
54
  map[normalized] = { code, text: ROUTE_TEXT[code] };
55
55
  };
56
- assign("po", RouteCode["Oral route"]);
57
- assign("oral", RouteCode["Oral route"]);
58
- assign("by mouth", RouteCode["Oral route"]);
59
- assign("per os", RouteCode["Oral route"]);
60
- assign("sl", RouteCode["Sublingual route"]);
61
- assign("s.l.", RouteCode["Sublingual route"]);
62
- assign("sublingual", RouteCode["Sublingual route"]);
63
- assign("buccal", RouteCode["Buccal route"]);
64
- assign("inh", RouteCode["Respiratory tract route (qualifier value)"]);
65
- assign("inhalation", RouteCode["Respiratory tract route (qualifier value)"]);
66
- assign("inhaled", RouteCode["Respiratory tract route (qualifier value)"]);
67
- assign("iv", RouteCode["Intravenous route"]);
68
- assign("ivp", RouteCode["Intravenous route"]);
69
- assign("ivpb", RouteCode["Intravenous route"]);
70
- assign("iv push", RouteCode["Intravenous route"]);
71
- assign("iv bolus", RouteCode["Intravenous route"]);
72
- assign("iv drip", RouteCode["Intravenous route"]);
73
- assign("intravenous", RouteCode["Intravenous route"]);
74
- assign("im", RouteCode["Intramuscular route"]);
75
- assign("im injection", RouteCode["Intramuscular route"]);
76
- assign("intramuscular", RouteCode["Intramuscular route"]);
77
- assign("sc", RouteCode["Subcutaneous route"]);
78
- assign("sq", RouteCode["Subcutaneous route"]);
79
- assign("subq", RouteCode["Subcutaneous route"]);
80
- assign("subcut", RouteCode["Subcutaneous route"]);
81
- assign("subcutaneous", RouteCode["Subcutaneous route"]);
82
- assign("in", RouteCode["Nasal route"]);
83
- assign("intranasal", RouteCode["Nasal route"]);
84
- assign("nasal", RouteCode["Nasal route"]);
85
- assign("top", RouteCode["Topical route"]);
86
- assign("topical", RouteCode["Topical route"]);
87
- assign("td", RouteCode["Transdermal route"]);
88
- assign("patch", RouteCode["Transdermal route"]);
89
- assign("transdermal", RouteCode["Transdermal route"]);
90
- assign("pr", RouteCode["Per rectum"]);
91
- assign("rectal", RouteCode["Per rectum"]);
92
- assign("pv", RouteCode["Per vagina"]);
93
- assign("vaginal", RouteCode["Per vagina"]);
94
- assign("oph", RouteCode["Ophthalmic route"]);
95
- assign("ophth", RouteCode["Ophthalmic route"]);
96
- assign("ophthalmic", RouteCode["Ophthalmic route"]);
97
- assign("ocular", RouteCode["Ophthalmic route"]);
98
- assign("intravitreal", RouteCode["Intravitreal route (qualifier value)"]);
99
- assign("intravitreal injection", RouteCode["Intravitreal route (qualifier value)"]);
100
- assign("ivt", RouteCode["Intravitreal route (qualifier value)"]);
56
+ const registerVariants = (value, code) => {
57
+ if (!value)
58
+ return;
59
+ assign(value, code);
60
+ const withoutParens = value
61
+ .replace(/[()]/g, " ")
62
+ .replace(/\s+/g, " ")
63
+ .trim();
64
+ assign(withoutParens, code);
65
+ const withoutCommas = value
66
+ .replace(/,/g, " ")
67
+ .replace(/\s+/g, " ")
68
+ .trim();
69
+ assign(withoutCommas, code);
70
+ const withoutPunctuation = value
71
+ .replace(/[().,-]/g, " ")
72
+ .replace(/\s+/g, " ")
73
+ .trim();
74
+ assign(withoutPunctuation, code);
75
+ };
76
+ registerVariants("po", RouteCode["Oral route"]);
77
+ registerVariants("oral", RouteCode["Oral route"]);
78
+ registerVariants("by mouth", RouteCode["Oral route"]);
79
+ registerVariants("per os", RouteCode["Oral route"]);
80
+ registerVariants("sl", RouteCode["Sublingual route"]);
81
+ registerVariants("s.l.", RouteCode["Sublingual route"]);
82
+ registerVariants("sublingual", RouteCode["Sublingual route"]);
83
+ registerVariants("buccal", RouteCode["Buccal route"]);
84
+ registerVariants("inh", RouteCode["Respiratory tract route (qualifier value)"]);
85
+ registerVariants("inhalation", RouteCode["Respiratory tract route (qualifier value)"]);
86
+ registerVariants("inhaled", RouteCode["Respiratory tract route (qualifier value)"]);
87
+ registerVariants("iv", RouteCode["Intravenous route"]);
88
+ registerVariants("ivp", RouteCode["Intravenous route"]);
89
+ registerVariants("ivpb", RouteCode["Intravenous route"]);
90
+ registerVariants("iv push", RouteCode["Intravenous route"]);
91
+ registerVariants("iv bolus", RouteCode["Intravenous route"]);
92
+ registerVariants("iv drip", RouteCode["Intravenous route"]);
93
+ registerVariants("intravenous", RouteCode["Intravenous route"]);
94
+ registerVariants("im", RouteCode["Intramuscular route"]);
95
+ registerVariants("im injection", RouteCode["Intramuscular route"]);
96
+ registerVariants("intramuscular", RouteCode["Intramuscular route"]);
97
+ registerVariants("sc", RouteCode["Subcutaneous route"]);
98
+ registerVariants("sq", RouteCode["Subcutaneous route"]);
99
+ registerVariants("subq", RouteCode["Subcutaneous route"]);
100
+ registerVariants("subcut", RouteCode["Subcutaneous route"]);
101
+ registerVariants("subcutaneous", RouteCode["Subcutaneous route"]);
102
+ registerVariants("in", RouteCode["Nasal route"]);
103
+ registerVariants("intranasal", RouteCode["Nasal route"]);
104
+ registerVariants("nasal", RouteCode["Nasal route"]);
105
+ registerVariants("top", RouteCode["Topical route"]);
106
+ registerVariants("topical", RouteCode["Topical route"]);
107
+ registerVariants("td", RouteCode["Transdermal route"]);
108
+ registerVariants("patch", RouteCode["Transdermal route"]);
109
+ registerVariants("transdermal", RouteCode["Transdermal route"]);
110
+ registerVariants("pr", RouteCode["Per rectum"]);
111
+ registerVariants("rectal", RouteCode["Per rectum"]);
112
+ registerVariants("pv", RouteCode["Per vagina"]);
113
+ registerVariants("vaginal", RouteCode["Per vagina"]);
114
+ registerVariants("oph", RouteCode["Ophthalmic route"]);
115
+ registerVariants("ophth", RouteCode["Ophthalmic route"]);
116
+ registerVariants("ophthalmic", RouteCode["Ophthalmic route"]);
117
+ registerVariants("ocular", RouteCode["Ophthalmic route"]);
118
+ registerVariants("intravitreal", RouteCode["Intravitreal route (qualifier value)"]);
119
+ registerVariants("intravitreal injection", RouteCode["Intravitreal route (qualifier value)"]);
120
+ registerVariants("ivt", RouteCode["Intravitreal route (qualifier value)"]);
101
121
  for (const [routeCode, meta] of ROUTE_SNOMED_ENTRIES) {
102
122
  const display = meta.display.toLowerCase();
103
- assign(display, routeCode);
123
+ registerVariants(display, routeCode);
104
124
  const withoutQualifier = display.replace(/\s*\(qualifier value\)/g, "").trim();
105
- assign(withoutQualifier, routeCode);
125
+ registerVariants(withoutQualifier, routeCode);
106
126
  const withoutSuffix = withoutQualifier
107
127
  .replace(/\b(route|use)\b/g, "")
108
128
  .replace(/\s+/g, " ")
109
129
  .trim();
110
- assign(withoutSuffix, routeCode);
130
+ registerVariants(withoutSuffix, routeCode);
111
131
  const withoutPer = withoutSuffix.replace(/^per\s+/, "").trim();
112
- assign(withoutPer, routeCode);
132
+ registerVariants(withoutPer, routeCode);
113
133
  }
114
134
  return map;
115
135
  })();
package/dist/parser.js CHANGED
@@ -609,7 +609,7 @@ export function parseInternal(input, options) {
609
609
  }
610
610
  // Process tokens sequentially
611
611
  const tryRouteSynonym = (startIndex) => {
612
- const maxSpan = Math.min(5, tokens.length - startIndex);
612
+ const maxSpan = Math.min(24, tokens.length - startIndex);
613
613
  for (let span = maxSpan; span >= 1; span--) {
614
614
  const slice = tokens.slice(startIndex, startIndex + span);
615
615
  if (slice.some((part) => internal.consumed.has(part.index))) {
@@ -0,0 +1,12 @@
1
+ import { ParseOptions } from "./types";
2
+ export interface SuggestSigOptions extends ParseOptions {
3
+ /**
4
+ * Maximum number of suggestions to return. Defaults to 10 when not supplied.
5
+ */
6
+ limit?: number;
7
+ /**
8
+ * Optional custom PRN reasons to use when generating suggestions.
9
+ */
10
+ prnReasons?: readonly string[];
11
+ }
12
+ export declare function suggestSig(input: string, options?: SuggestSigOptions): string[];
@@ -0,0 +1,223 @@
1
+ import { inferUnitFromContext } from "./context";
2
+ const DEFAULT_LIMIT = 10;
3
+ const DEFAULT_UNIT_ROUTE_ORDER = [
4
+ { unit: "tab", route: "po" },
5
+ { unit: "cap", route: "po" },
6
+ { unit: "mL", route: "po" },
7
+ { unit: "mg", route: "po" },
8
+ { unit: "puff", route: "inh" },
9
+ { unit: "spray", route: "in" },
10
+ { unit: "drop", route: "oph" },
11
+ { unit: "suppository", route: "pr" },
12
+ { unit: "patch", route: "transdermal" },
13
+ { unit: "g", route: "topical" }
14
+ ];
15
+ const DEFAULT_ROUTE_BY_UNIT = {
16
+ tab: "po",
17
+ tabs: "po",
18
+ tablet: "po",
19
+ cap: "po",
20
+ capsule: "po",
21
+ ml: "po",
22
+ mg: "po",
23
+ puff: "inh",
24
+ puffs: "inh",
25
+ spray: "in",
26
+ sprays: "in",
27
+ drop: "oph",
28
+ drops: "oph",
29
+ suppository: "pr",
30
+ suppositories: "pr",
31
+ patch: "transdermal",
32
+ patches: "transdermal",
33
+ g: "topical"
34
+ };
35
+ const FREQUENCY_CODES = ["qd", "bid", "tid", "qid"];
36
+ const INTERVAL_CODES = ["q4h", "q6h", "q8h"];
37
+ const WHEN_TOKENS = ["ac", "pc", "hs", "am", "pm"];
38
+ const CORE_WHEN_TOKENS = ["pc", "ac", "hs"];
39
+ const FREQUENCY_NUMBERS = [1, 2, 3, 4];
40
+ const FREQ_TOKEN_BY_NUMBER = {
41
+ 1: "qd",
42
+ 2: "bid",
43
+ 3: "tid",
44
+ 4: "qid",
45
+ };
46
+ const DEFAULT_PRN_REASONS = [
47
+ "pain",
48
+ "nausea",
49
+ "itching",
50
+ "anxiety",
51
+ "sleep",
52
+ "cough",
53
+ "fever",
54
+ "spasm",
55
+ "constipation",
56
+ "dyspnea",
57
+ ];
58
+ const DEFAULT_DOSE_COUNTS = ["1", "2"];
59
+ function normalizeKey(value) {
60
+ return value.trim().toLowerCase();
61
+ }
62
+ function normalizeSpacing(value) {
63
+ return value
64
+ .trim()
65
+ .replace(/\s+/g, " ");
66
+ }
67
+ function buildUnitRoutePairs(contextUnit) {
68
+ const pairs = [];
69
+ const seen = new Set();
70
+ const addPair = (unit, route) => {
71
+ if (!unit) {
72
+ return;
73
+ }
74
+ const cleanUnit = unit.trim();
75
+ if (!cleanUnit) {
76
+ return;
77
+ }
78
+ const normalizedUnit = cleanUnit.toLowerCase();
79
+ const resolvedRoute = route ?? DEFAULT_ROUTE_BY_UNIT[normalizedUnit] ?? "po";
80
+ const key = `${normalizedUnit}::${resolvedRoute.toLowerCase()}`;
81
+ if (seen.has(key)) {
82
+ return;
83
+ }
84
+ seen.add(key);
85
+ pairs.push({ unit: cleanUnit, route: resolvedRoute });
86
+ };
87
+ if (contextUnit) {
88
+ const normalized = normalizeKey(contextUnit);
89
+ addPair(contextUnit, DEFAULT_ROUTE_BY_UNIT[normalized]);
90
+ }
91
+ for (const pair of DEFAULT_UNIT_ROUTE_ORDER) {
92
+ addPair(pair.unit, pair.route);
93
+ }
94
+ return pairs;
95
+ }
96
+ function buildPrnReasons(customReasons) {
97
+ const reasons = new Set();
98
+ const add = (reason) => {
99
+ if (!reason) {
100
+ return;
101
+ }
102
+ const normalized = normalizeSpacing(reason.toLowerCase());
103
+ if (!normalized) {
104
+ return;
105
+ }
106
+ reasons.add(normalized);
107
+ };
108
+ if (customReasons) {
109
+ for (const reason of customReasons) {
110
+ add(reason);
111
+ }
112
+ }
113
+ for (const reason of DEFAULT_PRN_REASONS) {
114
+ add(reason);
115
+ }
116
+ return [...reasons];
117
+ }
118
+ function extractDoseValuesFromInput(input) {
119
+ const matches = input.match(/\d+(?:\.\d+)?(?:\/\d+(?:\.\d+)?)?/g);
120
+ if (!matches) {
121
+ return [];
122
+ }
123
+ const values = new Set();
124
+ for (const match of matches) {
125
+ if (!match) {
126
+ continue;
127
+ }
128
+ values.add(match);
129
+ }
130
+ return [...values];
131
+ }
132
+ function buildDoseValues(input) {
133
+ const dynamicValues = extractDoseValuesFromInput(input);
134
+ const values = new Set();
135
+ for (const value of dynamicValues) {
136
+ values.add(value);
137
+ }
138
+ for (const value of DEFAULT_DOSE_COUNTS) {
139
+ values.add(value);
140
+ }
141
+ return [...values];
142
+ }
143
+ function generateCandidateSignatures(pairs, doseValues, prnReasons) {
144
+ const suggestions = [];
145
+ const seen = new Set();
146
+ const push = (value) => {
147
+ const normalized = normalizeSpacing(value);
148
+ if (!normalized) {
149
+ return;
150
+ }
151
+ const key = normalizeKey(normalized);
152
+ if (seen.has(key)) {
153
+ return;
154
+ }
155
+ seen.add(key);
156
+ suggestions.push(normalized);
157
+ };
158
+ for (const pair of pairs) {
159
+ for (const code of FREQUENCY_CODES) {
160
+ for (const dose of doseValues) {
161
+ push(`${dose} ${pair.unit} ${pair.route} ${code}`);
162
+ }
163
+ push(`${pair.route} ${code}`);
164
+ }
165
+ for (const interval of INTERVAL_CODES) {
166
+ for (const dose of doseValues) {
167
+ push(`${dose} ${pair.unit} ${pair.route} ${interval}`);
168
+ for (const reason of prnReasons) {
169
+ push(`${dose} ${pair.unit} ${pair.route} ${interval} prn ${reason}`);
170
+ }
171
+ }
172
+ push(`${pair.route} ${interval}`);
173
+ }
174
+ for (const freq of FREQUENCY_NUMBERS) {
175
+ const freqToken = FREQ_TOKEN_BY_NUMBER[freq];
176
+ push(`1x${freq} ${pair.route} ${freqToken}`);
177
+ for (const when of CORE_WHEN_TOKENS) {
178
+ push(`1x${freq} ${pair.route} ${when}`);
179
+ }
180
+ }
181
+ for (const when of WHEN_TOKENS) {
182
+ for (const dose of doseValues) {
183
+ push(`${dose} ${pair.unit} ${pair.route} ${when}`);
184
+ }
185
+ push(`${pair.route} ${when}`);
186
+ }
187
+ for (const reason of prnReasons) {
188
+ push(`1 ${pair.unit} ${pair.route} prn ${reason}`);
189
+ }
190
+ }
191
+ return suggestions;
192
+ }
193
+ function matchesPrefix(candidate, prefix, prefixCompact) {
194
+ if (!prefix) {
195
+ return true;
196
+ }
197
+ const normalizedCandidate = candidate.toLowerCase();
198
+ if (normalizedCandidate.startsWith(prefix)) {
199
+ return true;
200
+ }
201
+ const compactCandidate = normalizedCandidate.replace(/\s+/g, "");
202
+ return compactCandidate.startsWith(prefixCompact);
203
+ }
204
+ export function suggestSig(input, options) {
205
+ const limit = options?.limit ?? DEFAULT_LIMIT;
206
+ const prefix = normalizeSpacing(input.toLowerCase());
207
+ const prefixCompact = prefix.replace(/\s+/g, "");
208
+ const contextUnit = inferUnitFromContext(options?.context ?? undefined);
209
+ const pairs = buildUnitRoutePairs(contextUnit);
210
+ const doseValues = buildDoseValues(input);
211
+ const prnReasons = buildPrnReasons(options?.prnReasons);
212
+ const candidates = generateCandidateSignatures(pairs, doseValues, prnReasons);
213
+ const results = [];
214
+ for (const candidate of candidates) {
215
+ if (matchesPrefix(candidate, prefix, prefixCompact)) {
216
+ results.push(candidate);
217
+ }
218
+ if (results.length >= limit) {
219
+ break;
220
+ }
221
+ }
222
+ return results;
223
+ }
package/dist/types.d.ts CHANGED
@@ -167,6 +167,7 @@ export declare enum SNOMEDCTRouteCodes {
167
167
  "Intrapulmonary route (qualifier value)" = "420201002",
168
168
  "Mucous fistula route (qualifier value)" = "420204005",
169
169
  "Nasoduodenal route (qualifier value)" = "420218003",
170
+ "Body cavity route" = "420254004",
170
171
  "A route that begins within a non-pathologic hollow cavity, such as that of the abdominal cavity or uterus." = "420254004",
171
172
  "Intraventricular route - cardiac (qualifier value)" = "420287000",
172
173
  "Intracerebroventricular route (qualifier value)" = "420719007",
package/dist/types.js CHANGED
@@ -148,6 +148,7 @@ export var SNOMEDCTRouteCodes;
148
148
  SNOMEDCTRouteCodes["Intrapulmonary route (qualifier value)"] = "420201002";
149
149
  SNOMEDCTRouteCodes["Mucous fistula route (qualifier value)"] = "420204005";
150
150
  SNOMEDCTRouteCodes["Nasoduodenal route (qualifier value)"] = "420218003";
151
+ SNOMEDCTRouteCodes["Body cavity route"] = "420254004";
151
152
  SNOMEDCTRouteCodes["A route that begins within a non-pathologic hollow cavity, such as that of the abdominal cavity or uterus."] = "420254004";
152
153
  SNOMEDCTRouteCodes["Intraventricular route - cardiac (qualifier value)"] = "420287000";
153
154
  SNOMEDCTRouteCodes["Intracerebroventricular route (qualifier value)"] = "420719007";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",