ezmedicationinput 0.1.38 → 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.
- package/README.md +2 -0
- package/dist/index.js +194 -2
- package/dist/maps.js +1 -0
- package/dist/parser.js +76 -8
- package/dist/schedule.js +214 -24
- package/dist/suggest.js +135 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -184,6 +184,8 @@ Highlights:
|
|
|
184
184
|
- Supports multiple timing tokens in sequence (e.g. `1 tab po morn hs`).
|
|
185
185
|
- Surfaces PRN reasons from built-ins or custom `prnReasons` entries while
|
|
186
186
|
preserving numeric doses pulled from the typed prefix.
|
|
187
|
+
- When `enableMealDashSyntax` is enabled, suggests dash-based meal patterns
|
|
188
|
+
(e.g. `1-0-1`, `1-0-0-1 ac`) only when dash syntax is being typed.
|
|
187
189
|
|
|
188
190
|
## Dictionaries
|
|
189
191
|
|
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
|
|
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
|
|
427
|
+
appendParseResult(results, result, options);
|
|
236
428
|
updateCarryForward(carry, internal);
|
|
237
429
|
}
|
|
238
430
|
const legacy = resolveLegacyParseResult(results, input, options);
|
package/dist/maps.js
CHANGED
|
@@ -668,6 +668,7 @@ exports.EVENT_TIMING_TOKENS = {
|
|
|
668
668
|
pcd: types_1.EventTiming["After Lunch"],
|
|
669
669
|
pcv: types_1.EventTiming["After Dinner"],
|
|
670
670
|
wm: types_1.EventTiming.Meal,
|
|
671
|
+
c: types_1.EventTiming.Meal,
|
|
671
672
|
"with meals": types_1.EventTiming.Meal,
|
|
672
673
|
"with meal": types_1.EventTiming.Meal,
|
|
673
674
|
"with food": types_1.EventTiming.Meal,
|
package/dist/parser.js
CHANGED
|
@@ -780,26 +780,84 @@ function parseTimeToFhir(timeStr) {
|
|
|
780
780
|
const m = minute < 10 ? `0${minute}` : `${minute}`;
|
|
781
781
|
return `${h}:${m}:00`;
|
|
782
782
|
}
|
|
783
|
+
function extractAttachedAtTimeToken(lower) {
|
|
784
|
+
if (lower.length <= 1) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
if (lower.charAt(0) === "@") {
|
|
788
|
+
const candidate = lower.slice(1);
|
|
789
|
+
return parseTimeToFhir(candidate) ? candidate : undefined;
|
|
790
|
+
}
|
|
791
|
+
if (lower.startsWith("at") && lower.length > 2 && /^\d/.test(lower.charAt(2))) {
|
|
792
|
+
const candidate = lower.slice(2);
|
|
793
|
+
return parseTimeToFhir(candidate) ? candidate : undefined;
|
|
794
|
+
}
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
function isAtPrefixToken(lower) {
|
|
798
|
+
return lower === "@" || lower === "at" || extractAttachedAtTimeToken(lower) !== undefined;
|
|
799
|
+
}
|
|
783
800
|
function tryParseTimeBasedSchedule(internal, tokens, index) {
|
|
784
801
|
const token = tokens[index];
|
|
785
802
|
if (internal.consumed.has(token.index))
|
|
786
803
|
return false;
|
|
787
|
-
|
|
804
|
+
const attachedAtTime = extractAttachedAtTimeToken(token.lower);
|
|
805
|
+
const isAtPrefix = isAtPrefixToken(token.lower);
|
|
788
806
|
if (!isAtPrefix && !/^\d/.test(token.lower))
|
|
789
807
|
return false;
|
|
790
808
|
let nextIndex = index;
|
|
791
|
-
if (isAtPrefix)
|
|
792
|
-
nextIndex++;
|
|
793
809
|
const times = [];
|
|
794
810
|
const consumedIndices = [];
|
|
795
811
|
const timeTokens = [];
|
|
796
|
-
if (
|
|
812
|
+
if (token.lower === "@" || token.lower === "at") {
|
|
813
|
+
consumedIndices.push(index);
|
|
814
|
+
nextIndex++;
|
|
815
|
+
}
|
|
816
|
+
else if (attachedAtTime) {
|
|
817
|
+
let timeStr = attachedAtTime;
|
|
818
|
+
const lookaheadIndices = [];
|
|
819
|
+
if (!timeStr.includes("am") && !timeStr.includes("pm")) {
|
|
820
|
+
const ampmToken = tokens[index + 1];
|
|
821
|
+
if (ampmToken &&
|
|
822
|
+
!internal.consumed.has(ampmToken.index) &&
|
|
823
|
+
(ampmToken.lower === "am" || ampmToken.lower === "pm")) {
|
|
824
|
+
timeStr += ampmToken.lower;
|
|
825
|
+
lookaheadIndices.push(index + 1);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const compactTime = parseTimeToFhir(timeStr);
|
|
829
|
+
if (!compactTime) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
times.push(compactTime);
|
|
833
|
+
timeTokens.push(timeStr);
|
|
797
834
|
consumedIndices.push(index);
|
|
835
|
+
for (const idx of lookaheadIndices) {
|
|
836
|
+
consumedIndices.push(idx);
|
|
837
|
+
}
|
|
838
|
+
nextIndex = index + 1 + lookaheadIndices.length;
|
|
798
839
|
}
|
|
799
840
|
while (nextIndex < tokens.length) {
|
|
800
841
|
const nextToken = tokens[nextIndex];
|
|
801
842
|
if (!nextToken || internal.consumed.has(nextToken.index))
|
|
802
843
|
break;
|
|
844
|
+
if ((nextToken.lower === "," || nextToken.lower === "and") && times.length > 0) {
|
|
845
|
+
const peekToken = tokens[nextIndex + 1];
|
|
846
|
+
if (peekToken && !internal.consumed.has(peekToken.index)) {
|
|
847
|
+
let peekStr = peekToken.lower;
|
|
848
|
+
const ampmToken = tokens[nextIndex + 2];
|
|
849
|
+
if (ampmToken &&
|
|
850
|
+
!internal.consumed.has(ampmToken.index) &&
|
|
851
|
+
(ampmToken.lower === "am" || ampmToken.lower === "pm")) {
|
|
852
|
+
peekStr += ampmToken.lower;
|
|
853
|
+
}
|
|
854
|
+
if (parseTimeToFhir(peekStr)) {
|
|
855
|
+
consumedIndices.push(nextIndex);
|
|
856
|
+
nextIndex++;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
803
861
|
let timeStr = nextToken.lower;
|
|
804
862
|
let lookaheadIndices = [];
|
|
805
863
|
// Look ahead for am/pm if current token is just a number or doesn't have am/pm
|
|
@@ -1066,6 +1124,11 @@ function splitToken(token) {
|
|
|
1066
1124
|
if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
|
|
1067
1125
|
return [token];
|
|
1068
1126
|
}
|
|
1127
|
+
const compactPoMeal = token.match(/^(po)(ac|pc|c)$/i);
|
|
1128
|
+
if (compactPoMeal) {
|
|
1129
|
+
const [, po, meal] = compactPoMeal;
|
|
1130
|
+
return [po, meal];
|
|
1131
|
+
}
|
|
1069
1132
|
if (/^[A-Za-z]+$/.test(token)) {
|
|
1070
1133
|
return [token];
|
|
1071
1134
|
}
|
|
@@ -1077,6 +1140,11 @@ function splitToken(token) {
|
|
|
1077
1140
|
const match = token.match(/^([0-9]+(?:\.[0-9]+)?)([A-Za-z]+)$/);
|
|
1078
1141
|
if (match) {
|
|
1079
1142
|
const [, num, unit] = match;
|
|
1143
|
+
const compactPoMealUnit = unit.match(/^(po)(ac|pc|c)$/i);
|
|
1144
|
+
if (compactPoMealUnit) {
|
|
1145
|
+
const [, po, meal] = compactPoMealUnit;
|
|
1146
|
+
return [num, po, meal];
|
|
1147
|
+
}
|
|
1080
1148
|
if (!/^x\d+/i.test(unit) && !/^q\d+/i.test(unit)) {
|
|
1081
1149
|
return [num, unit];
|
|
1082
1150
|
}
|
|
@@ -1514,7 +1582,7 @@ function isTimingAnchorOrPrefix(tokens, index, prnReasonStart) {
|
|
|
1514
1582
|
maps_1.TIMING_ABBREVIATIONS[lower] ||
|
|
1515
1583
|
(comboKey && COMBO_EVENT_TIMINGS[comboKey]) ||
|
|
1516
1584
|
(lower === "pc" || lower === "ac" || lower === "after" || lower === "before") ||
|
|
1517
|
-
(lower
|
|
1585
|
+
(isAtPrefixToken(lower) || lower === "on" || lower === "with") ||
|
|
1518
1586
|
/^\d/.test(lower));
|
|
1519
1587
|
}
|
|
1520
1588
|
function parseAnchorSequence(internal, tokens, index, prefixCode) {
|
|
@@ -1963,11 +2031,11 @@ function parseInternal(input, options) {
|
|
|
1963
2031
|
: types_1.EventTiming["Before Meal"]);
|
|
1964
2032
|
continue;
|
|
1965
2033
|
}
|
|
1966
|
-
if (token.lower
|
|
1967
|
-
if (
|
|
2034
|
+
if (isAtPrefixToken(token.lower) || token.lower === "on" || token.lower === "with") {
|
|
2035
|
+
if (tryParseTimeBasedSchedule(internal, tokens, i)) {
|
|
1968
2036
|
continue;
|
|
1969
2037
|
}
|
|
1970
|
-
if (
|
|
2038
|
+
if (parseAnchorSequence(internal, tokens, i)) {
|
|
1971
2039
|
continue;
|
|
1972
2040
|
}
|
|
1973
2041
|
// If none of the above consume it, and it's a known anchor prefix, mark it
|
package/dist/schedule.js
CHANGED
|
@@ -280,6 +280,44 @@ function getLocalWeekday(date, timeZone) {
|
|
|
280
280
|
throw new Error(`Unexpected weekday token: ${formatted}`);
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
function getLocalDayNumber(date, timeZone) {
|
|
284
|
+
const { year, month, day } = getTimeParts(date, timeZone);
|
|
285
|
+
return Math.floor(Date.UTC(year, month - 1, day) / (24 * 60 * 60 * 1000));
|
|
286
|
+
}
|
|
287
|
+
function getLocalMonthIndex(date, timeZone) {
|
|
288
|
+
const { year, month } = getTimeParts(date, timeZone);
|
|
289
|
+
return year * 12 + (month - 1);
|
|
290
|
+
}
|
|
291
|
+
function isDateAlignedToPeriodCycle(candidateDay, anchorDay, repeat, timeZone) {
|
|
292
|
+
const period = repeat.period;
|
|
293
|
+
const periodUnit = repeat.periodUnit;
|
|
294
|
+
if (!period || period <= 0 || !periodUnit) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (periodUnit === "d") {
|
|
298
|
+
const deltaDays = getLocalDayNumber(candidateDay, timeZone) - getLocalDayNumber(anchorDay, timeZone);
|
|
299
|
+
return deltaDays >= 0 && deltaDays % period === 0;
|
|
300
|
+
}
|
|
301
|
+
if (periodUnit === "wk") {
|
|
302
|
+
const deltaDays = getLocalDayNumber(candidateDay, timeZone) - getLocalDayNumber(anchorDay, timeZone);
|
|
303
|
+
if (deltaDays < 0) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const deltaWeeks = Math.floor(deltaDays / 7);
|
|
307
|
+
return deltaWeeks % period === 0;
|
|
308
|
+
}
|
|
309
|
+
if (periodUnit === "mo") {
|
|
310
|
+
const deltaMonths = getLocalMonthIndex(candidateDay, timeZone) - getLocalMonthIndex(anchorDay, timeZone);
|
|
311
|
+
return deltaMonths >= 0 && deltaMonths % period === 0;
|
|
312
|
+
}
|
|
313
|
+
if (periodUnit === "a") {
|
|
314
|
+
const candidateYear = getTimeParts(candidateDay, timeZone).year;
|
|
315
|
+
const anchorYear = getTimeParts(anchorDay, timeZone).year;
|
|
316
|
+
const deltaYears = candidateYear - anchorYear;
|
|
317
|
+
return deltaYears >= 0 && deltaYears % period === 0;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
283
321
|
/** Parses arbitrary string/Date inputs into a valid Date instance. */
|
|
284
322
|
function coerceDate(value, label) {
|
|
285
323
|
const date = value instanceof Date ? value : new Date(value);
|
|
@@ -706,12 +744,29 @@ function nextDueDoses(dosage, options) {
|
|
|
706
744
|
(!repeat.frequency ||
|
|
707
745
|
repeat.periodUnit !== "d" ||
|
|
708
746
|
(repeat.frequency === 1 && repeat.period > 1));
|
|
709
|
-
|
|
747
|
+
const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
|
|
748
|
+
if (treatAsInterval && supportsDayFilteredInterval) {
|
|
710
749
|
// True interval schedules advance from the order start in fixed units. The
|
|
711
750
|
// timing.code remains advisory so we only rely on the period/unit fields.
|
|
712
751
|
const candidates = generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone, dayFilter, enforceDayFilter, orderedAt);
|
|
713
752
|
return candidates;
|
|
714
753
|
}
|
|
754
|
+
if (enforceDayFilter &&
|
|
755
|
+
repeat.period &&
|
|
756
|
+
repeat.periodUnit &&
|
|
757
|
+
(repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
|
|
758
|
+
return generateDayFilteredPeriodSeries({
|
|
759
|
+
repeat,
|
|
760
|
+
timeZone,
|
|
761
|
+
dayFilter,
|
|
762
|
+
anchorDay: startOfLocalDay(baseTime, timeZone),
|
|
763
|
+
startDay: from,
|
|
764
|
+
from,
|
|
765
|
+
orderedAt,
|
|
766
|
+
limit: effectiveLimit,
|
|
767
|
+
defaultClock: toLocalClock(baseTime, timeZone)
|
|
768
|
+
}).slice(0, effectiveLimit);
|
|
769
|
+
}
|
|
715
770
|
if (repeat.frequency && repeat.period && repeat.periodUnit) {
|
|
716
771
|
// Pure frequency schedules (e.g., BID/TID) rely on institution clocks that
|
|
717
772
|
// clinicians expect. These can be overridden via configuration when
|
|
@@ -851,7 +906,8 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
|
|
|
851
906
|
(!repeat.frequency ||
|
|
852
907
|
repeat.periodUnit !== "d" ||
|
|
853
908
|
(repeat.frequency === 1 && repeat.period > 1));
|
|
854
|
-
|
|
909
|
+
const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
|
|
910
|
+
if (treatAsInterval && supportsDayFilteredInterval) {
|
|
855
911
|
const increment = createIntervalStepper(repeat, timeZone);
|
|
856
912
|
if (!increment) {
|
|
857
913
|
return count;
|
|
@@ -875,6 +931,24 @@ function derivePriorCountFromHistory(timing, repeat, config, orderedAt, from, ti
|
|
|
875
931
|
}
|
|
876
932
|
return count;
|
|
877
933
|
}
|
|
934
|
+
if (enforceDayFilter &&
|
|
935
|
+
repeat.period &&
|
|
936
|
+
repeat.periodUnit &&
|
|
937
|
+
(repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
|
|
938
|
+
const generated = generateDayFilteredPeriodSeries({
|
|
939
|
+
repeat,
|
|
940
|
+
timeZone,
|
|
941
|
+
dayFilter,
|
|
942
|
+
anchorDay: startOfLocalDay(orderedAt, timeZone),
|
|
943
|
+
startDay: orderedAt,
|
|
944
|
+
from: orderedAt,
|
|
945
|
+
to: from,
|
|
946
|
+
orderedAt,
|
|
947
|
+
limit: normalizedCount !== null && normalizedCount !== void 0 ? normalizedCount : 31 * 365,
|
|
948
|
+
defaultClock: toLocalClock(orderedAt, timeZone)
|
|
949
|
+
});
|
|
950
|
+
return count + generated.length;
|
|
951
|
+
}
|
|
878
952
|
if (repeat.frequency && repeat.period && repeat.periodUnit) {
|
|
879
953
|
const clocks = resolveFrequencyClocks(timing, config);
|
|
880
954
|
if (clocks.length === 0) {
|
|
@@ -966,6 +1040,10 @@ function generateIntervalSeries(baseTime, from, effectiveLimit, repeat, timeZone
|
|
|
966
1040
|
}
|
|
967
1041
|
/**
|
|
968
1042
|
* Builds a function that advances a Date according to repeat.period/unit.
|
|
1043
|
+
*
|
|
1044
|
+
* @param repeat FHIR repeat object containing `period` and `periodUnit`.
|
|
1045
|
+
* @param timeZone IANA timezone used for calendar-aware month/year stepping.
|
|
1046
|
+
* @returns Stepper function advancing one interval, or `null` when unsupported.
|
|
969
1047
|
*/
|
|
970
1048
|
function createIntervalStepper(repeat, timeZone) {
|
|
971
1049
|
const { period, periodUnit } = repeat;
|
|
@@ -993,7 +1071,14 @@ function createIntervalStepper(repeat, timeZone) {
|
|
|
993
1071
|
}
|
|
994
1072
|
return null;
|
|
995
1073
|
}
|
|
996
|
-
/**
|
|
1074
|
+
/**
|
|
1075
|
+
* Adds calendar months while respecting varying month lengths and DST.
|
|
1076
|
+
*
|
|
1077
|
+
* @param date Starting instant.
|
|
1078
|
+
* @param months Number of calendar months to add.
|
|
1079
|
+
* @param timeZone IANA timezone used for wall-clock preservation.
|
|
1080
|
+
* @returns Shifted date preserving local clock and clamped day-of-month.
|
|
1081
|
+
*/
|
|
997
1082
|
function addCalendarMonths(date, months, timeZone) {
|
|
998
1083
|
const { year, month, day, hour, minute, second } = getTimeParts(date, timeZone);
|
|
999
1084
|
const targetMonthIndex = month - 1 + months;
|
|
@@ -1014,8 +1099,114 @@ function addCalendarMonths(date, months, timeZone) {
|
|
|
1014
1099
|
}
|
|
1015
1100
|
return final;
|
|
1016
1101
|
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Determines whether interval stepping can safely combine with a day-of-week filter.
|
|
1104
|
+
*
|
|
1105
|
+
* @param repeat FHIR timing repeat object containing period unit metadata.
|
|
1106
|
+
* @param enforceDayFilter Whether a `dayOfWeek` filter is active for this schedule.
|
|
1107
|
+
* @returns `true` when interval stepping should be used directly; otherwise fallback logic is required.
|
|
1108
|
+
*/
|
|
1109
|
+
function isDayFilteredIntervalSupported(repeat, enforceDayFilter) {
|
|
1110
|
+
if (!enforceDayFilter) {
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
return (repeat.periodUnit === "s" ||
|
|
1114
|
+
repeat.periodUnit === "min" ||
|
|
1115
|
+
repeat.periodUnit === "h" ||
|
|
1116
|
+
repeat.periodUnit === "d");
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Expands weekly/monthly/yearly day-filtered schedules into concrete dose timestamps.
|
|
1120
|
+
*
|
|
1121
|
+
* @param options Configuration describing bounds, cadence, and clock defaults.
|
|
1122
|
+
* @param options.repeat FHIR repeat block driving cadence and cycle alignment.
|
|
1123
|
+
* @param options.timeZone IANA timezone used for local weekday and clock resolution.
|
|
1124
|
+
* @param options.dayFilter Set of lowercased weekdays that are allowed (e.g. `mon`, `tue`).
|
|
1125
|
+
* @param options.anchorDay Cycle anchor day used to determine period alignment.
|
|
1126
|
+
* @param options.startDay First local day to begin scanning.
|
|
1127
|
+
* @param options.from Inclusive lower bound for candidate timestamps.
|
|
1128
|
+
* @param options.to Optional exclusive upper bound for candidate timestamps.
|
|
1129
|
+
* @param options.orderedAt Optional lower bound representing the order start.
|
|
1130
|
+
* @param options.limit Maximum number of timestamps to emit.
|
|
1131
|
+
* @param options.defaultClock Default local clock (`HH:MM:SS`) when no explicit clock is provided.
|
|
1132
|
+
* @returns Sorted zoned ISO timestamps matching the requested cadence and bounds.
|
|
1133
|
+
*/
|
|
1134
|
+
function generateDayFilteredPeriodSeries(options) {
|
|
1135
|
+
const { repeat, timeZone, dayFilter, anchorDay, startDay, from, to, orderedAt, limit, defaultClock } = options;
|
|
1136
|
+
if (limit <= 0) {
|
|
1137
|
+
return [];
|
|
1138
|
+
}
|
|
1139
|
+
const results = [];
|
|
1140
|
+
const seen = new Set();
|
|
1141
|
+
const clocks = [defaultClock !== null && defaultClock !== void 0 ? defaultClock : "08:00:00"];
|
|
1142
|
+
let currentDay = startOfLocalDay(startDay, timeZone);
|
|
1143
|
+
let iterations = 0;
|
|
1144
|
+
const startMs = currentDay.getTime();
|
|
1145
|
+
const estimatedDays = to
|
|
1146
|
+
? Math.max(1, Math.ceil((to.getTime() - startMs) / (24 * 60 * 60 * 1000)))
|
|
1147
|
+
: limit * 31;
|
|
1148
|
+
const maxIterations = Math.max(limit * 31, estimatedDays + 31);
|
|
1149
|
+
while (results.length < limit &&
|
|
1150
|
+
iterations < maxIterations &&
|
|
1151
|
+
(!to || currentDay < to)) {
|
|
1152
|
+
const weekday = getLocalWeekday(currentDay, timeZone);
|
|
1153
|
+
const inPeriodCycle = isDateAlignedToPeriodCycle(currentDay, anchorDay, repeat, timeZone);
|
|
1154
|
+
if (inPeriodCycle && dayFilter.has(weekday)) {
|
|
1155
|
+
for (const clock of clocks) {
|
|
1156
|
+
const zoned = makeZonedDateFromDay(currentDay, timeZone, clock);
|
|
1157
|
+
if (!zoned) {
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
if (zoned < from) {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (to && zoned >= to) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
if (orderedAt && zoned < orderedAt) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const iso = formatZonedIso(zoned, timeZone);
|
|
1170
|
+
if (!seen.has(iso)) {
|
|
1171
|
+
seen.add(iso);
|
|
1172
|
+
results.push(iso);
|
|
1173
|
+
if (results.length === limit) {
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
currentDay = addLocalDays(currentDay, 1, timeZone);
|
|
1180
|
+
iterations += 1;
|
|
1181
|
+
}
|
|
1182
|
+
return results;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Formats a date into a local `HH:MM:SS` clock for the supplied timezone.
|
|
1186
|
+
*
|
|
1187
|
+
* @param date Source instant.
|
|
1188
|
+
* @param timeZone IANA timezone to interpret the instant.
|
|
1189
|
+
* @returns Local wall-clock time in `HH:MM:SS` format.
|
|
1190
|
+
*/
|
|
1191
|
+
function toLocalClock(date, timeZone) {
|
|
1192
|
+
const parts = getTimeParts(date, timeZone);
|
|
1193
|
+
const twoDigits = (value) => (value < 10 ? `0${value}` : `${value}`);
|
|
1194
|
+
const h = twoDigits(parts.hour);
|
|
1195
|
+
const m = twoDigits(parts.minute);
|
|
1196
|
+
const s = twoDigits(parts.second);
|
|
1197
|
+
return `${h}:${m}:${s}`;
|
|
1198
|
+
}
|
|
1017
1199
|
/**
|
|
1018
1200
|
* Internal helper to count dose events within a time range.
|
|
1201
|
+
*
|
|
1202
|
+
* @param dosage Dosage definition with timing metadata.
|
|
1203
|
+
* @param from Inclusive lower time bound for counting.
|
|
1204
|
+
* @param to Exclusive upper time bound for counting.
|
|
1205
|
+
* @param config Scheduling configuration (timezone, clocks, offsets).
|
|
1206
|
+
* @param baseTime Anchor instant used for interval alignment.
|
|
1207
|
+
* @param orderedAt Optional order timestamp used as an additional lower bound.
|
|
1208
|
+
* @param limit Optional hard cap on emitted candidates to avoid runaway loops.
|
|
1209
|
+
* @returns Number of unique dose events in the requested window.
|
|
1019
1210
|
*/
|
|
1020
1211
|
function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limit) {
|
|
1021
1212
|
var _a, _b, _c;
|
|
@@ -1098,9 +1289,9 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
|
|
|
1098
1289
|
!!repeat.periodUnit &&
|
|
1099
1290
|
(!repeat.frequency ||
|
|
1100
1291
|
repeat.periodUnit !== "d" ||
|
|
1101
|
-
(repeat.frequency === 1 && repeat.period > 1))
|
|
1102
|
-
|
|
1103
|
-
if (treatAsInterval) {
|
|
1292
|
+
(repeat.frequency === 1 && repeat.period > 1));
|
|
1293
|
+
const supportsDayFilteredInterval = isDayFilteredIntervalSupported(repeat, enforceDayFilter);
|
|
1294
|
+
if (treatAsInterval && supportsDayFilteredInterval) {
|
|
1104
1295
|
const increment = createIntervalStepper(repeat, timeZone);
|
|
1105
1296
|
if (!increment)
|
|
1106
1297
|
return count;
|
|
@@ -1149,24 +1340,23 @@ function countScheduleEvents(dosage, from, to, config, baseTime, orderedAt, limi
|
|
|
1149
1340
|
}
|
|
1150
1341
|
}
|
|
1151
1342
|
// Fallback for dayOfWeek with period/periodUnit but no explicit frequency/clocks
|
|
1152
|
-
if (enforceDayFilter &&
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
return count;
|
|
1343
|
+
if (enforceDayFilter &&
|
|
1344
|
+
repeat.period &&
|
|
1345
|
+
repeat.periodUnit &&
|
|
1346
|
+
(repeat.periodUnit === "wk" || repeat.periodUnit === "mo" || repeat.periodUnit === "a")) {
|
|
1347
|
+
const generated = generateDayFilteredPeriodSeries({
|
|
1348
|
+
repeat,
|
|
1349
|
+
timeZone,
|
|
1350
|
+
dayFilter,
|
|
1351
|
+
anchorDay: startOfLocalDay(baseTime, timeZone),
|
|
1352
|
+
startDay: from,
|
|
1353
|
+
from,
|
|
1354
|
+
to,
|
|
1355
|
+
orderedAt,
|
|
1356
|
+
limit: limit !== null && limit !== void 0 ? limit : 365 * 31,
|
|
1357
|
+
defaultClock: toLocalClock(baseTime, timeZone)
|
|
1358
|
+
});
|
|
1359
|
+
return count + generated.length;
|
|
1170
1360
|
}
|
|
1171
1361
|
return count;
|
|
1172
1362
|
}
|
package/dist/suggest.js
CHANGED
|
@@ -729,6 +729,131 @@ function matchesPrefix(_candidate, candidateLower, context) {
|
|
|
729
729
|
}
|
|
730
730
|
return false;
|
|
731
731
|
}
|
|
732
|
+
function collectMatchedCandidates(candidates, limit, matcher) {
|
|
733
|
+
const suggestions = [];
|
|
734
|
+
const seen = new Set();
|
|
735
|
+
for (const candidate of candidates) {
|
|
736
|
+
const value = normalizeSpacing(candidate);
|
|
737
|
+
if (!value) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const lower = value.toLowerCase();
|
|
741
|
+
if (seen.has(lower)) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!matcher(value, lower)) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
seen.add(lower);
|
|
748
|
+
suggestions.push(value);
|
|
749
|
+
if (suggestions.length >= limit) {
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return suggestions;
|
|
754
|
+
}
|
|
755
|
+
function buildMealDashCoreVariants(prefixCore) {
|
|
756
|
+
var _a;
|
|
757
|
+
if (!prefixCore.includes("-") || prefixCore.includes("--")) {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
const slots = prefixCore.split("-");
|
|
761
|
+
if (slots.length < 2 || slots.length > 4) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
if (!/^[0-9]+(?:\.[0-9]+)?$/.test((_a = slots[0]) !== null && _a !== void 0 ? _a : "")) {
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
for (let i = 1; i < slots.length; i += 1) {
|
|
768
|
+
const slot = slots[i];
|
|
769
|
+
if (slot.length === 0) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (!/^[0-9]+(?:\.[0-9]+)?$/.test(slot)) {
|
|
773
|
+
return [];
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const variants = [];
|
|
777
|
+
const seen = new Set();
|
|
778
|
+
const addVariant = (value) => {
|
|
779
|
+
if (!seen.has(value)) {
|
|
780
|
+
seen.add(value);
|
|
781
|
+
variants.push(value);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
const first = slots[0];
|
|
785
|
+
const fillBase = (targetLength) => {
|
|
786
|
+
const values = new Array(targetLength).fill("0");
|
|
787
|
+
values[0] = first;
|
|
788
|
+
for (let i = 1; i < targetLength; i += 1) {
|
|
789
|
+
if (i < slots.length && slots[i] !== "") {
|
|
790
|
+
values[i] = slots[i];
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return values;
|
|
794
|
+
};
|
|
795
|
+
const base3 = fillBase(3);
|
|
796
|
+
const missingThird = slots.length < 3 || slots[2] === "";
|
|
797
|
+
if (missingThird && (slots.length === 1 || slots[1] === "" || slots[1] === "0")) {
|
|
798
|
+
const mirror = [...base3];
|
|
799
|
+
mirror[2] = first;
|
|
800
|
+
addVariant(mirror.join("-"));
|
|
801
|
+
}
|
|
802
|
+
addVariant(base3.join("-"));
|
|
803
|
+
const base4 = fillBase(4);
|
|
804
|
+
const missingFourth = slots.length < 4 || slots[3] === "";
|
|
805
|
+
if (missingFourth &&
|
|
806
|
+
(slots.length === 1 ||
|
|
807
|
+
slots[1] === "" ||
|
|
808
|
+
(slots[1] === "0" && (slots.length < 3 || slots[2] === "" || slots[2] === "0")))) {
|
|
809
|
+
const mirror = [...base4];
|
|
810
|
+
mirror[3] = first;
|
|
811
|
+
addVariant(mirror.join("-"));
|
|
812
|
+
}
|
|
813
|
+
addVariant(base4.join("-"));
|
|
814
|
+
return variants;
|
|
815
|
+
}
|
|
816
|
+
function suggestMealDashSyntax(prefix, limit, matcher) {
|
|
817
|
+
if (!prefix.includes("-")) {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
const match = prefix.match(/^(\d+(?:-\d*){0,3})(?:\s+(ac|pc))?$/);
|
|
821
|
+
if (!match) {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
const core = match[1];
|
|
825
|
+
const relation = match[2];
|
|
826
|
+
const coreVariants = buildMealDashCoreVariants(core);
|
|
827
|
+
if (coreVariants.length === 0) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
const suffixes = relation ? [` ${relation}`] : ["", " ac", " pc"];
|
|
831
|
+
const candidates = [];
|
|
832
|
+
for (const variant of coreVariants) {
|
|
833
|
+
for (const suffix of suffixes) {
|
|
834
|
+
candidates.push(`${variant}${suffix}`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return collectMatchedCandidates(candidates, limit, matcher);
|
|
838
|
+
}
|
|
839
|
+
function suggestCompactOralMealTiming(prefix, limit, matcher) {
|
|
840
|
+
var _a, _b;
|
|
841
|
+
const match = prefix.match(/^(\d+(?:\.\d+)?)\s*(?:po\s*(c|ac|pc)|po(c|ac|pc))$/);
|
|
842
|
+
if (!match) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
const dose = normalizeSpacing(match[1]);
|
|
846
|
+
const timing = ((_b = (_a = match[2]) !== null && _a !== void 0 ? _a : match[3]) !== null && _b !== void 0 ? _b : "").toLowerCase();
|
|
847
|
+
const orderedTimings = timing === "c"
|
|
848
|
+
? ["c", "ac", "pc"]
|
|
849
|
+
: timing === "ac"
|
|
850
|
+
? ["ac", "c", "pc"]
|
|
851
|
+
: timing === "pc"
|
|
852
|
+
? ["pc", "c", "ac"]
|
|
853
|
+
: ["c", "ac", "pc"];
|
|
854
|
+
const candidates = orderedTimings.map((token) => `${dose} po ${token}`);
|
|
855
|
+
return collectMatchedCandidates(candidates, limit, matcher);
|
|
856
|
+
}
|
|
732
857
|
function suggestSig(input, options) {
|
|
733
858
|
var _a, _b;
|
|
734
859
|
const limit = (_a = options === null || options === void 0 ? void 0 : options.limit) !== null && _a !== void 0 ? _a : DEFAULT_LIMIT;
|
|
@@ -768,5 +893,15 @@ function suggestSig(input, options) {
|
|
|
768
893
|
const timeTokens = buildTimeTokens(input);
|
|
769
894
|
const whenSequences = PRECOMPUTED_WHEN_SEQUENCES;
|
|
770
895
|
const matcher = (candidate, candidateLower) => matchesPrefix(candidate, candidateLower, prefixContext);
|
|
896
|
+
const compactOralSuggestions = suggestCompactOralMealTiming(prefix, limit, matcher);
|
|
897
|
+
if (compactOralSuggestions && compactOralSuggestions.length > 0) {
|
|
898
|
+
return compactOralSuggestions;
|
|
899
|
+
}
|
|
900
|
+
if (options === null || options === void 0 ? void 0 : options.enableMealDashSyntax) {
|
|
901
|
+
const mealDashSuggestions = suggestMealDashSyntax(prefix, limit, matcher);
|
|
902
|
+
if (mealDashSuggestions && mealDashSuggestions.length > 0) {
|
|
903
|
+
return mealDashSuggestions;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
771
906
|
return generateCandidateDirections(pairs, doseValues, prnReasons, intervalTokens, timeTokens, whenSequences, limit, matcher);
|
|
772
907
|
}
|