ezmedicationinput 0.1.39 → 0.1.40

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.
Files changed (2) hide show
  1. package/dist/index.js +194 -2
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -58,6 +58,15 @@ Object.defineProperty(exports, "DEFAULT_BODY_SITE_SNOMED_SOURCE", { enumerable:
58
58
  Object.defineProperty(exports, "DEFAULT_ROUTE_SYNONYMS", { enumerable: true, get: function () { return maps_1.DEFAULT_ROUTE_SYNONYMS; } });
59
59
  Object.defineProperty(exports, "DEFAULT_UNIT_BY_ROUTE", { enumerable: true, get: function () { return maps_1.DEFAULT_UNIT_BY_ROUTE; } });
60
60
  Object.defineProperty(exports, "KNOWN_DOSAGE_FORMS_TO_DOSE", { enumerable: true, get: function () { return maps_1.KNOWN_DOSAGE_FORMS_TO_DOSE; } });
61
+ const REPEAT_NON_ANCHOR_KEYS = [
62
+ "count",
63
+ "frequency",
64
+ "frequencyMax",
65
+ "period",
66
+ "periodMax",
67
+ "periodUnit",
68
+ "offset"
69
+ ];
61
70
  function parseMealDashValues(token) {
62
71
  if (!/^[0-9]+(?:\.[0-9]+)?(?:-[0-9]+(?:\.[0-9]+)?){2,3}$/.test(token)) {
63
72
  return undefined;
@@ -155,6 +164,189 @@ function toSegmentMeta(segments) {
155
164
  range: { start: segment.start, end: segment.end }
156
165
  }));
157
166
  }
167
+ /**
168
+ * Deep equality helper for plain JSON-like parser output objects.
169
+ *
170
+ * @param left Left-side value.
171
+ * @param right Right-side value.
172
+ * @returns `true` when both values are structurally equal.
173
+ */
174
+ function deepEqual(left, right) {
175
+ if (left === right) {
176
+ return true;
177
+ }
178
+ if (left === null || right === null) {
179
+ return left === right;
180
+ }
181
+ if (Array.isArray(left) || Array.isArray(right)) {
182
+ if (!Array.isArray(left) || !Array.isArray(right)) {
183
+ return false;
184
+ }
185
+ if (left.length !== right.length) {
186
+ return false;
187
+ }
188
+ for (let i = 0; i < left.length; i += 1) {
189
+ if (!deepEqual(left[i], right[i])) {
190
+ return false;
191
+ }
192
+ }
193
+ return true;
194
+ }
195
+ if (typeof left !== "object" || typeof right !== "object") {
196
+ return false;
197
+ }
198
+ const leftRecord = left;
199
+ const rightRecord = right;
200
+ const leftKeys = Object.keys(leftRecord).filter((key) => leftRecord[key] !== undefined);
201
+ const rightKeys = Object.keys(rightRecord).filter((key) => rightRecord[key] !== undefined);
202
+ if (leftKeys.length !== rightKeys.length) {
203
+ return false;
204
+ }
205
+ for (const key of leftKeys) {
206
+ if (!Object.prototype.hasOwnProperty.call(rightRecord, key)) {
207
+ return false;
208
+ }
209
+ if (!deepEqual(leftRecord[key], rightRecord[key])) {
210
+ return false;
211
+ }
212
+ }
213
+ return true;
214
+ }
215
+ /**
216
+ * Compares two string arrays as sets.
217
+ *
218
+ * @param left Left array.
219
+ * @param right Right array.
220
+ * @returns `true` when both arrays contain the same unique values.
221
+ */
222
+ function sameStringSet(left, right) {
223
+ const a = left !== null && left !== void 0 ? left : [];
224
+ const b = right !== null && right !== void 0 ? right : [];
225
+ if (a.length !== b.length) {
226
+ return false;
227
+ }
228
+ const set = new Set(a);
229
+ if (set.size !== b.length) {
230
+ return false;
231
+ }
232
+ for (const value of b) {
233
+ if (!set.has(value)) {
234
+ return false;
235
+ }
236
+ }
237
+ return true;
238
+ }
239
+ /**
240
+ * Determines whether a repeat block only uses merge-safe anchor fields.
241
+ *
242
+ * @param repeat FHIR timing repeat payload.
243
+ * @returns `true` when repeat contains only `when`/`timeOfDay`/`dayOfWeek`.
244
+ */
245
+ function isMergeableAnchorRepeat(repeat) {
246
+ if (!repeat) {
247
+ return true;
248
+ }
249
+ for (const key of REPEAT_NON_ANCHOR_KEYS) {
250
+ if (repeat[key] !== undefined) {
251
+ return false;
252
+ }
253
+ }
254
+ return true;
255
+ }
256
+ /**
257
+ * Checks whether two parsed items can be merged without changing semantics.
258
+ *
259
+ * @param base Existing merged item candidate.
260
+ * @param next Incoming parsed item.
261
+ * @returns `true` when both items differ only by merge-safe timing anchors.
262
+ */
263
+ function canMergeTimingOnly(base, next) {
264
+ const baseTiming = base.fhir.timing;
265
+ const nextTiming = next.fhir.timing;
266
+ const baseRepeat = baseTiming === null || baseTiming === void 0 ? void 0 : baseTiming.repeat;
267
+ const nextRepeat = nextTiming === null || nextTiming === void 0 ? void 0 : nextTiming.repeat;
268
+ if (!baseRepeat || !nextRepeat) {
269
+ return false;
270
+ }
271
+ if (!isMergeableAnchorRepeat(baseRepeat) || !isMergeableAnchorRepeat(nextRepeat)) {
272
+ return false;
273
+ }
274
+ if (!sameStringSet(baseRepeat.dayOfWeek, nextRepeat.dayOfWeek)) {
275
+ return false;
276
+ }
277
+ if (!deepEqual(baseTiming === null || baseTiming === void 0 ? void 0 : baseTiming.code, nextTiming === null || nextTiming === void 0 ? void 0 : nextTiming.code)) {
278
+ return false;
279
+ }
280
+ if (!deepEqual(baseTiming === null || baseTiming === void 0 ? void 0 : baseTiming.event, nextTiming === null || nextTiming === void 0 ? void 0 : nextTiming.event)) {
281
+ return false;
282
+ }
283
+ return (deepEqual(base.fhir.doseAndRate, next.fhir.doseAndRate) &&
284
+ deepEqual(base.fhir.route, next.fhir.route) &&
285
+ deepEqual(base.fhir.site, next.fhir.site) &&
286
+ deepEqual(base.fhir.additionalInstruction, next.fhir.additionalInstruction) &&
287
+ deepEqual(base.fhir.asNeededBoolean, next.fhir.asNeededBoolean) &&
288
+ deepEqual(base.fhir.asNeededFor, next.fhir.asNeededFor));
289
+ }
290
+ /**
291
+ * Returns a stable unique list preserving first-seen order.
292
+ *
293
+ * @param values Input values.
294
+ * @returns Deduplicated values in insertion order.
295
+ */
296
+ function uniqueStrings(values) {
297
+ const seen = new Set();
298
+ const output = [];
299
+ for (const value of values) {
300
+ if (!seen.has(value)) {
301
+ seen.add(value);
302
+ output.push(value);
303
+ }
304
+ }
305
+ return output;
306
+ }
307
+ /**
308
+ * Merges two parse results that are known to be timing-compatible.
309
+ *
310
+ * @param base Existing merged result.
311
+ * @param next Next result to fold into `base`.
312
+ * @param options Parse options used to render localized text.
313
+ * @returns New merged parse result.
314
+ */
315
+ function mergeParseResults(base, next, options) {
316
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
317
+ const baseRepeat = (_b = (_a = base.fhir.timing) === null || _a === void 0 ? void 0 : _a.repeat) !== null && _b !== void 0 ? _b : {};
318
+ const nextRepeat = (_d = (_c = next.fhir.timing) === null || _c === void 0 ? void 0 : _c.repeat) !== null && _d !== void 0 ? _d : {};
319
+ const mergedWhen = uniqueStrings([...((_e = baseRepeat.when) !== null && _e !== void 0 ? _e : []), ...((_f = nextRepeat.when) !== null && _f !== void 0 ? _f : [])]);
320
+ const mergedTimeOfDay = uniqueStrings([...((_g = baseRepeat.timeOfDay) !== null && _g !== void 0 ? _g : []), ...((_h = nextRepeat.timeOfDay) !== null && _h !== void 0 ? _h : [])]).sort();
321
+ const mergedRepeat = Object.assign(Object.assign(Object.assign(Object.assign({}, baseRepeat), (nextRepeat.dayOfWeek ? { dayOfWeek: nextRepeat.dayOfWeek } : {})), (mergedWhen.length ? { when: mergedWhen } : {})), (mergedTimeOfDay.length ? { timeOfDay: mergedTimeOfDay } : {}));
322
+ const mergedFhir = Object.assign(Object.assign({}, base.fhir), { timing: Object.assign(Object.assign({}, ((_j = base.fhir.timing) !== null && _j !== void 0 ? _j : {})), { repeat: mergedRepeat }) });
323
+ const shortText = formatSig(mergedFhir, "short", options);
324
+ const longText = formatSig(mergedFhir, "long", options);
325
+ mergedFhir.text = longText;
326
+ return {
327
+ fhir: mergedFhir,
328
+ shortText,
329
+ longText,
330
+ warnings: uniqueStrings([...((_k = base.warnings) !== null && _k !== void 0 ? _k : []), ...((_l = next.warnings) !== null && _l !== void 0 ? _l : [])]),
331
+ meta: Object.assign(Object.assign({}, base.meta), { consumedTokens: uniqueStrings([...((_m = base.meta.consumedTokens) !== null && _m !== void 0 ? _m : []), ...((_o = next.meta.consumedTokens) !== null && _o !== void 0 ? _o : [])]), leftoverText: uniqueStrings([base.meta.leftoverText, next.meta.leftoverText].filter((value) => !!value)).join(" ").trim() || undefined, siteLookups: [...((_p = base.meta.siteLookups) !== null && _p !== void 0 ? _p : []), ...((_q = next.meta.siteLookups) !== null && _q !== void 0 ? _q : [])], prnReasonLookups: [...((_r = base.meta.prnReasonLookups) !== null && _r !== void 0 ? _r : []), ...((_s = next.meta.prnReasonLookups) !== null && _s !== void 0 ? _s : [])] })
332
+ };
333
+ }
334
+ /**
335
+ * Appends a parsed segment result to the batch, reusing the current item when
336
+ * timing-only expansion can be represented as a single dosage element.
337
+ *
338
+ * @param items Accumulated batch items.
339
+ * @param next Newly parsed segment result.
340
+ * @param options Parse options used to format merged text.
341
+ */
342
+ function appendParseResult(items, next, options) {
343
+ const previous = items[items.length - 1];
344
+ if (previous && canMergeTimingOnly(previous, next)) {
345
+ items[items.length - 1] = mergeParseResults(previous, next, options);
346
+ return;
347
+ }
348
+ items.push(next);
349
+ }
158
350
  function parseSig(input, options) {
159
351
  const segments = expandMealDashSegments((0, segment_1.splitSigSegments)(input), options);
160
352
  const carry = {};
@@ -166,7 +358,7 @@ function parseSig(input, options) {
166
358
  (0, parser_1.applySiteCoding)(internal, options);
167
359
  const result = buildParseResult(internal, options);
168
360
  rebaseParseResult(result, input, segment.start);
169
- results.push(result);
361
+ appendParseResult(results, result, options);
170
362
  updateCarryForward(carry, internal);
171
363
  }
172
364
  const legacy = resolveLegacyParseResult(results, input, options);
@@ -232,7 +424,7 @@ function parseSigAsync(input, options) {
232
424
  yield (0, parser_1.applySiteCodingAsync)(internal, options);
233
425
  const result = buildParseResult(internal, options);
234
426
  rebaseParseResult(result, input, segment.start);
235
- results.push(result);
427
+ appendParseResult(results, result, options);
236
428
  updateCarryForward(carry, internal);
237
429
  }
238
430
  const legacy = resolveLegacyParseResult(results, input, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",