ezmedicationinput 0.1.52 → 0.1.54

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.
@@ -6,6 +6,7 @@ type CompoundDoseUnit = {
6
6
  head: string;
7
7
  tails: string[];
8
8
  tailSequences?: string[][];
9
+ requiresSiteContext?: boolean;
9
10
  unit: string;
10
11
  };
11
12
  export declare const SITE_ANCHORS: Set<string>;
package/dist/index.cjs CHANGED
@@ -2252,6 +2252,14 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2252
2252
  names: ["axilla", "axillae", "armpit", "armpits"],
2253
2253
  definition: { coding: { code: "34797008", display: "Axilla structure" }, routeHint: RouteCode["Topical route"] }
2254
2254
  },
2255
+ {
2256
+ names: ["axillary hair", "armpit hair", "armpit hairs", "underarm hair", "underarm hairs"],
2257
+ definition: {
2258
+ coding: { code: "75703003", display: "Structure of hair of axilla" },
2259
+ text: "axillary hair",
2260
+ routeHint: RouteCode["Topical route"]
2261
+ }
2262
+ },
2255
2263
  {
2256
2264
  names: ["groin"],
2257
2265
  definition: { coding: { code: "26893007", display: "Inguinal region structure" }, routeHint: RouteCode["Topical route"] }
@@ -2322,6 +2330,71 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2322
2330
  names: ["face"],
2323
2331
  definition: { coding: { code: "89545001", display: "Face" }, routeHint: RouteCode["Topical route"] }
2324
2332
  },
2333
+ {
2334
+ names: ["eyebrow", "brow"],
2335
+ definition: {
2336
+ coding: { code: "392262008", display: "Eyebrow structure" },
2337
+ text: "eyebrow",
2338
+ routeHint: RouteCode["Topical route"]
2339
+ }
2340
+ },
2341
+ {
2342
+ names: ["eyebrows", "brows"],
2343
+ definition: {
2344
+ coding: { code: "392262008", display: "Eyebrow structure" },
2345
+ text: "eyebrows",
2346
+ routeHint: RouteCode["Topical route"]
2347
+ }
2348
+ },
2349
+ {
2350
+ names: ["left eyebrow", "left brow"],
2351
+ definition: {
2352
+ coding: { code: "722011002", display: "Structure of eyebrow of left eye region" },
2353
+ text: "left eyebrow",
2354
+ routeHint: RouteCode["Topical route"]
2355
+ }
2356
+ },
2357
+ {
2358
+ names: ["right eyebrow", "right brow"],
2359
+ definition: {
2360
+ coding: { code: "722012009", display: "Structure of eyebrow of right eye region" },
2361
+ text: "right eyebrow",
2362
+ routeHint: RouteCode["Topical route"]
2363
+ }
2364
+ },
2365
+ {
2366
+ names: [
2367
+ "both eyebrows",
2368
+ "bilateral eyebrows",
2369
+ "each eyebrow",
2370
+ "left and right eyebrow",
2371
+ "left and right eyebrows",
2372
+ "right and left eyebrow",
2373
+ "right and left eyebrows",
2374
+ "left right eyebrow",
2375
+ "left right eyebrows"
2376
+ ],
2377
+ definition: {
2378
+ coding: {
2379
+ code: buildSnomedBodySiteLateralityPostcoordinationCode(
2380
+ "392262008",
2381
+ SNOMED_CT_BILATERAL_QUALIFIER_CODE
2382
+ ),
2383
+ display: "both eyebrows"
2384
+ },
2385
+ text: "both eyebrows",
2386
+ administrationTargetCount: 2,
2387
+ routeHint: RouteCode["Topical route"]
2388
+ }
2389
+ },
2390
+ {
2391
+ names: ["eyebrow hair", "eyebrow hairs", "brow hair", "brow hairs"],
2392
+ definition: {
2393
+ coding: { code: "392261001", display: "Hair structure of eyebrow" },
2394
+ text: "eyebrow hair",
2395
+ routeHint: RouteCode["Topical route"]
2396
+ }
2397
+ },
2325
2398
  {
2326
2399
  names: ["eyelid", "eyelids"],
2327
2400
  definition: { coding: { code: "80243003", display: "Eyelid" }, routeHint: RouteCode["Ophthalmic route"] }
@@ -2442,6 +2515,30 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2442
2515
  names: ["perineum"],
2443
2516
  definition: { coding: { code: "243990009", display: "Entire perineum" }, routeHint: RouteCode["Topical route"] }
2444
2517
  },
2518
+ {
2519
+ names: ["mustache", "moustache", "mustache hair", "moustache hair"],
2520
+ definition: {
2521
+ coding: { code: "256925006", display: "Structure of hair of mustache" },
2522
+ text: "mustache",
2523
+ routeHint: RouteCode["Topical route"]
2524
+ }
2525
+ },
2526
+ {
2527
+ names: ["beard", "beard hair", "facial hair"],
2528
+ definition: {
2529
+ coding: { code: "367576007", display: "Structure of beard hair" },
2530
+ text: "beard",
2531
+ routeHint: RouteCode["Topical route"]
2532
+ }
2533
+ },
2534
+ {
2535
+ names: ["pubic hair"],
2536
+ definition: {
2537
+ coding: { code: "75776007", display: "Structure of hair of pubis" },
2538
+ text: "pubic hair",
2539
+ routeHint: RouteCode["Topical route"]
2540
+ }
2541
+ },
2445
2542
  {
2446
2543
  names: ["skin"],
2447
2544
  definition: { coding: { code: "181469002", display: "Entire skin" }, routeHint: RouteCode["Topical route"] }
@@ -2452,6 +2549,38 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2452
2549
  coding: { code: "386045008", display: "Hair structure (body structure)" },
2453
2550
  routeHint: RouteCode["Topical route"]
2454
2551
  }
2552
+ },
2553
+ {
2554
+ names: ["joint"],
2555
+ definition: {
2556
+ coding: { code: "39352004", display: "Joint structure" },
2557
+ text: "joint",
2558
+ routeHint: RouteCode["Topical route"]
2559
+ }
2560
+ },
2561
+ {
2562
+ names: ["joints"],
2563
+ definition: {
2564
+ coding: { code: "81087007", display: "Joints" },
2565
+ text: "joints",
2566
+ routeHint: RouteCode["Topical route"]
2567
+ }
2568
+ },
2569
+ {
2570
+ names: ["finger joint", "finger joints"],
2571
+ definition: {
2572
+ coding: { code: "125682004", display: "Finger joint structure" },
2573
+ text: "finger joints",
2574
+ routeHint: RouteCode["Topical route"]
2575
+ }
2576
+ },
2577
+ {
2578
+ names: ["knuckle", "knuckles"],
2579
+ definition: {
2580
+ coding: { code: "70420003", display: "Metacarpophalangeal joint structure" },
2581
+ text: "knuckles",
2582
+ routeHint: RouteCode["Topical route"]
2583
+ }
2455
2584
  }
2456
2585
  ];
2457
2586
  var DEFAULT_BODY_SITE_SNOMED = objectFromEntries(
@@ -4363,6 +4492,7 @@ var lexical_classes_default = {
4363
4492
  "Use shampoo": "\u0E2A\u0E23\u0E30"
4364
4493
  },
4365
4494
  compoundDoseUnits: [
4495
+ { head: "cm", tails: [], requiresSiteContext: true, unit: "cm line" },
4366
4496
  { head: "cm", tails: ["ribbon", "ribbons"], unit: "cm ribbon" },
4367
4497
  { head: "cm", tails: ["strip", "strips"], unit: "cm strip" },
4368
4498
  { head: "cm", tails: ["line", "lines"], unit: "cm line" },
@@ -4371,6 +4501,8 @@ var lexical_classes_default = {
4371
4501
  { head: "fingertip", tails: ["unit", "units"], unit: "FTU" },
4372
4502
  { head: "eye", tails: ["drop", "drops"], unit: "drop" },
4373
4503
  { head: "pea-sized", tails: ["amount"], unit: "pea-sized amount" },
4504
+ { head: "pea-size", tails: ["amount"], unit: "pea-sized amount" },
4505
+ { head: "peasize", tails: [], tailSequences: [[]], unit: "pea-sized amount" },
4374
4506
  { head: "\u0E40\u0E21\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27", tails: [], unit: "pea-sized amount" },
4375
4507
  { head: "\u0E40\u0E21\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27\u0E40\u0E02\u0E35\u0E22\u0E27", tails: [], unit: "pea-sized amount" },
4376
4508
  { head: "\u0E40\u0E21\u0E25\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27", tails: [], unit: "pea-sized amount" },
@@ -4380,7 +4512,7 @@ var lexical_classes_default = {
4380
4512
  {
4381
4513
  head: "pea",
4382
4514
  tails: [],
4383
- tailSequences: [["sized", "amount"], ["size", "amount"]],
4515
+ tailSequences: [["sized", "amount"], ["size", "amount"], ["amount"], ["sized"], ["size"], []],
4384
4516
  unit: "pea-sized amount"
4385
4517
  },
4386
4518
  { head: "hand", tails: ["print", "prints"], unit: "handprint" },
@@ -15026,6 +15158,11 @@ var unit_terminology_default = {
15026
15158
  unit: "pea-sized amount",
15027
15159
  kind: "product_specific_amount",
15028
15160
  aliases: [
15161
+ "pea",
15162
+ "pea amount",
15163
+ "peasize",
15164
+ "pea-size",
15165
+ "pea size",
15029
15166
  "pea sized amount",
15030
15167
  "pea-sized",
15031
15168
  "pea sized",
@@ -15122,10 +15259,96 @@ var unit_terminology_default = {
15122
15259
  ]
15123
15260
  };
15124
15261
 
15262
+ // src/utils/units.ts
15263
+ var MASS_UNITS = {
15264
+ kg: 1e6,
15265
+ g: 1e3,
15266
+ mg: 1,
15267
+ mcg: 1e-3,
15268
+ ug: 1e-3,
15269
+ microg: 1e-3,
15270
+ ng: 1e-6
15271
+ };
15272
+ var VOLUME_UNITS = {
15273
+ l: 1e3,
15274
+ dl: 100,
15275
+ ml: 1,
15276
+ ul: 1e-3,
15277
+ microl: 1e-3,
15278
+ cm3: 1,
15279
+ tsp: 5,
15280
+ tbsp: 15
15281
+ };
15282
+ function getUnitCategory(unit) {
15283
+ if (!unit) return "other";
15284
+ const u = unit.toLowerCase();
15285
+ if (MASS_UNITS[u] !== void 0) return "mass";
15286
+ if (VOLUME_UNITS[u] !== void 0) return "volume";
15287
+ return "other";
15288
+ }
15289
+ function getBaseUnitFactor(unit) {
15290
+ var _a2, _b;
15291
+ if (!unit) return 1;
15292
+ const u = unit.toLowerCase();
15293
+ return (_b = (_a2 = MASS_UNITS[u]) != null ? _a2 : VOLUME_UNITS[u]) != null ? _b : 1;
15294
+ }
15295
+ function convertValue(value, fromUnit, toUnit, strength) {
15296
+ const f = fromUnit.toLowerCase();
15297
+ const t = toUnit.toLowerCase();
15298
+ if (f === t) return value;
15299
+ const fCat = getUnitCategory(f);
15300
+ const tCat = getUnitCategory(t);
15301
+ if (fCat === tCat && fCat !== "other") {
15302
+ const fFactor = getBaseUnitFactor(f);
15303
+ const tFactor = getBaseUnitFactor(t);
15304
+ return value * fFactor / tFactor;
15305
+ }
15306
+ if (strength && (fCat === "mass" && tCat === "volume" || fCat === "volume" && tCat === "mass")) {
15307
+ const numUnit = strength.numerator.unit.toLowerCase();
15308
+ const denUnit = strength.denominator.unit.toLowerCase();
15309
+ const numCat = getUnitCategory(numUnit);
15310
+ const denCat = getUnitCategory(denUnit);
15311
+ if (numCat !== denCat && numCat !== "other" && denCat !== "other") {
15312
+ const massSide = numCat === "mass" ? strength.numerator : strength.denominator;
15313
+ const volSide = numCat === "volume" ? strength.numerator : strength.denominator;
15314
+ const bridgeDensity = massSide.value * getBaseUnitFactor(massSide.unit) / (volSide.value * getBaseUnitFactor(volSide.unit));
15315
+ if (fCat === "mass") {
15316
+ const valueMg = value * getBaseUnitFactor(fromUnit);
15317
+ const valueMl = valueMg / bridgeDensity;
15318
+ return valueMl / getBaseUnitFactor(toUnit);
15319
+ } else {
15320
+ const valueMl = value * getBaseUnitFactor(fromUnit);
15321
+ const valueMg = valueMl * bridgeDensity;
15322
+ return valueMg / getBaseUnitFactor(toUnit);
15323
+ }
15324
+ }
15325
+ }
15326
+ return null;
15327
+ }
15328
+
15125
15329
  // src/unit-lexicon.ts
15126
15330
  var HOUSEHOLD_VOLUME_UNIT_SET = new Set(
15127
15331
  HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase())
15128
15332
  );
15333
+ var MASS_DISPENSED_SEMISOLID_DOSAGE_FORMS = /* @__PURE__ */ new Set([
15334
+ "cream",
15335
+ "ointment",
15336
+ "gel",
15337
+ "paste",
15338
+ "cutaneous paste",
15339
+ "vaginal cream",
15340
+ "vaginal gel",
15341
+ "oral gel",
15342
+ "oral paste",
15343
+ "oromucosal gel",
15344
+ "oromucosal paste",
15345
+ "dental gel",
15346
+ "dental paste",
15347
+ "gingival gel",
15348
+ "nasal gel",
15349
+ "eye gel",
15350
+ "eye ointment"
15351
+ ]);
15129
15352
  var DOSE_UNIT_TERMINOLOGY = unit_terminology_default.terms;
15130
15353
  var DOSE_UNIT_TERMINOLOGY_BY_KEY = /* @__PURE__ */ new Map();
15131
15354
  var DISCRETE_UNIT_KINDS = /* @__PURE__ */ new Set([
@@ -15188,6 +15411,47 @@ function unitApproximationOverride(unit, context) {
15188
15411
  }
15189
15412
  return void 0;
15190
15413
  }
15414
+ function normalizeDosageFormKey(form) {
15415
+ var _a2;
15416
+ const normalized = form == null ? void 0 : form.trim().toLowerCase();
15417
+ if (!normalized) {
15418
+ return void 0;
15419
+ }
15420
+ return (_a2 = KNOWN_DOSAGE_FORMS_TO_DOSE[normalized]) != null ? _a2 : normalized;
15421
+ }
15422
+ function getPreferredMassApproximationUnit(context) {
15423
+ var _a2;
15424
+ const normalizedDosageForm = normalizeDosageFormKey(context == null ? void 0 : context.dosageForm);
15425
+ if (!normalizedDosageForm || !MASS_DISPENSED_SEMISOLID_DOSAGE_FORMS.has(normalizedDosageForm)) {
15426
+ return void 0;
15427
+ }
15428
+ const containerUnit = (_a2 = context == null ? void 0 : context.containerUnit) == null ? void 0 : _a2.trim();
15429
+ if (containerUnit && getUnitCategory(containerUnit) === "mass") {
15430
+ return containerUnit;
15431
+ }
15432
+ const defaultUnit = normalizedDosageForm ? DEFAULT_UNIT_BY_NORMALIZED_FORM[normalizedDosageForm] : void 0;
15433
+ return defaultUnit && getUnitCategory(defaultUnit) === "mass" ? defaultUnit : void 0;
15434
+ }
15435
+ function bridgeApproximationToMassDispensedTopical(approximation, context) {
15436
+ const preferredMassUnit = getPreferredMassApproximationUnit(context);
15437
+ if (!preferredMassUnit || getUnitCategory(approximation.unit) !== "volume") {
15438
+ return approximation;
15439
+ }
15440
+ const sourceVolumeFactor = VOLUME_UNITS[approximation.unit.toLowerCase()];
15441
+ const targetMassFactor = MASS_UNITS[preferredMassUnit.toLowerCase()];
15442
+ if (!sourceVolumeFactor || !targetMassFactor) {
15443
+ return approximation;
15444
+ }
15445
+ const valueMl = approximation.value * sourceVolumeFactor;
15446
+ const valueG = valueMl;
15447
+ const convertedValue = valueG * MASS_UNITS.g / targetMassFactor;
15448
+ const bridgeBasis = "Mass-dispensed semisolid topical default bridge assumes 1 mL approximately equals 1 g unless a product-specific override is provided";
15449
+ return __spreadProps(__spreadValues({}, approximation), {
15450
+ value: convertedValue,
15451
+ unit: preferredMassUnit,
15452
+ basis: approximation.basis ? `${approximation.basis}; ${bridgeBasis}` : bridgeBasis
15453
+ });
15454
+ }
15191
15455
  function getDoseUnitTerminologyEntry(unit) {
15192
15456
  var _a2;
15193
15457
  if (!unit) {
@@ -15209,7 +15473,11 @@ function getDoseUnitApproximation(unit, context) {
15209
15473
  if (override) {
15210
15474
  return override;
15211
15475
  }
15212
- return terminologyEntry == null ? void 0 : terminologyEntry.approximateQuantity;
15476
+ const approximation = terminologyEntry == null ? void 0 : terminologyEntry.approximateQuantity;
15477
+ if (!approximation) {
15478
+ return void 0;
15479
+ }
15480
+ return bridgeApproximationToMassDispensedTopical(approximation, context);
15213
15481
  }
15214
15482
  function getDoseUnitSemantics(unit, context) {
15215
15483
  if (!unit) {
@@ -16456,6 +16724,64 @@ function productLexicalRule() {
16456
16724
  }
16457
16725
  function matchCompoundDoseUnit(context, start, lower) {
16458
16726
  var _a2;
16727
+ const MAX_SITE_CONTEXT_TOKENS = 3;
16728
+ const hasSiteMeaning = (lowerValue) => {
16729
+ var _a3, _b, _c;
16730
+ return Boolean(
16731
+ lowerValue && (DEFAULT_BODY_SITE_SNOMED[normalizeBodySiteKey(lowerValue)] || resolveBodySitePhrase(lowerValue, (_a3 = context.options) == null ? void 0 : _a3.siteCodeMap, {
16732
+ bodySiteContext: (_c = (_b = context.options) == null ? void 0 : _b.context) == null ? void 0 : _c.bodySiteContext
16733
+ }))
16734
+ );
16735
+ };
16736
+ const collectForwardSitePhrases = (phraseStart) => {
16737
+ const parts = [];
16738
+ const phrases = [];
16739
+ for (let index = phraseStart; index < context.limit && parts.length < MAX_SITE_CONTEXT_TOKENS; index += 1) {
16740
+ const token = context.tokens[index];
16741
+ if (!token || context.state.consumed.has(token.index)) {
16742
+ break;
16743
+ }
16744
+ const lowerValue = normalizeTokenLower(token);
16745
+ if (!lowerValue || isPunctuation(lowerValue)) {
16746
+ break;
16747
+ }
16748
+ parts.push(lowerValue);
16749
+ phrases.push(parts.join(" "));
16750
+ }
16751
+ return phrases;
16752
+ };
16753
+ const matchesSiteContext = () => {
16754
+ const next = context.tokens[start + 1];
16755
+ const nextLower = next && !context.state.consumed.has(next.index) ? normalizeTokenLower(next) : void 0;
16756
+ if (nextLower && (ROUTE_SITE_PREPOSITIONS.has(nextLower) || SITE_ANCHORS.has(nextLower))) {
16757
+ const followingSitePhrases = collectForwardSitePhrases(start + 2);
16758
+ if (followingSitePhrases.some((phrase) => hasSiteMeaning(phrase))) {
16759
+ return true;
16760
+ }
16761
+ }
16762
+ const previous = context.tokens[start - 1];
16763
+ const previousLower = previous && !context.state.consumed.has(previous.index) ? normalizeTokenLower(previous) : void 0;
16764
+ const trailingNumberBeforeUnit = Boolean(
16765
+ previousLower && /^[0-9]+(?:\.[0-9]+)?$/.test(previousLower)
16766
+ );
16767
+ const siteEnd = start - (trailingNumberBeforeUnit ? 2 : 1);
16768
+ for (let phraseLength = 1; phraseLength <= MAX_SITE_CONTEXT_TOKENS; phraseLength += 1) {
16769
+ const phraseStart = siteEnd - phraseLength + 1;
16770
+ if (phraseStart < 0) {
16771
+ break;
16772
+ }
16773
+ const precedingAnchor = context.tokens[phraseStart - 1];
16774
+ const precedingAnchorLower = precedingAnchor && !context.state.consumed.has(precedingAnchor.index) ? normalizeTokenLower(precedingAnchor) : void 0;
16775
+ if (!precedingAnchorLower || !ROUTE_SITE_PREPOSITIONS.has(precedingAnchorLower) && !SITE_ANCHORS.has(precedingAnchorLower)) {
16776
+ continue;
16777
+ }
16778
+ const phrase = collectForwardSitePhrases(phraseStart)[phraseLength - 1];
16779
+ if (hasSiteMeaning(phrase)) {
16780
+ return true;
16781
+ }
16782
+ }
16783
+ return false;
16784
+ };
16459
16785
  for (const compound of COMPOUND_DOSE_UNITS) {
16460
16786
  if (compound.head !== lower) {
16461
16787
  continue;
@@ -16485,6 +16811,10 @@ function matchCompoundDoseUnit(context, start, lower) {
16485
16811
  return head ? { unit: compound.unit, tokens: [head, next] } : void 0;
16486
16812
  }
16487
16813
  }
16814
+ if (compound.requiresSiteContext && matchesSiteContext()) {
16815
+ const head = context.tokens[start];
16816
+ return head ? { unit: compound.unit, tokens: [head] } : void 0;
16817
+ }
16488
16818
  }
16489
16819
  return void 0;
16490
16820
  }
@@ -19682,73 +20012,6 @@ function suggestSig(input, options) {
19682
20012
  );
19683
20013
  }
19684
20014
 
19685
- // src/utils/units.ts
19686
- var MASS_UNITS = {
19687
- kg: 1e6,
19688
- g: 1e3,
19689
- mg: 1,
19690
- mcg: 1e-3,
19691
- ug: 1e-3,
19692
- microg: 1e-3,
19693
- ng: 1e-6
19694
- };
19695
- var VOLUME_UNITS = {
19696
- l: 1e3,
19697
- dl: 100,
19698
- ml: 1,
19699
- ul: 1e-3,
19700
- microl: 1e-3,
19701
- cm3: 1,
19702
- tsp: 5,
19703
- tbsp: 15
19704
- };
19705
- function getUnitCategory(unit) {
19706
- if (!unit) return "other";
19707
- const u = unit.toLowerCase();
19708
- if (MASS_UNITS[u] !== void 0) return "mass";
19709
- if (VOLUME_UNITS[u] !== void 0) return "volume";
19710
- return "other";
19711
- }
19712
- function getBaseUnitFactor(unit) {
19713
- var _a2, _b;
19714
- if (!unit) return 1;
19715
- const u = unit.toLowerCase();
19716
- return (_b = (_a2 = MASS_UNITS[u]) != null ? _a2 : VOLUME_UNITS[u]) != null ? _b : 1;
19717
- }
19718
- function convertValue(value, fromUnit, toUnit, strength) {
19719
- const f = fromUnit.toLowerCase();
19720
- const t = toUnit.toLowerCase();
19721
- if (f === t) return value;
19722
- const fCat = getUnitCategory(f);
19723
- const tCat = getUnitCategory(t);
19724
- if (fCat === tCat && fCat !== "other") {
19725
- const fFactor = getBaseUnitFactor(f);
19726
- const tFactor = getBaseUnitFactor(t);
19727
- return value * fFactor / tFactor;
19728
- }
19729
- if (strength && (fCat === "mass" && tCat === "volume" || fCat === "volume" && tCat === "mass")) {
19730
- const numUnit = strength.numerator.unit.toLowerCase();
19731
- const denUnit = strength.denominator.unit.toLowerCase();
19732
- const numCat = getUnitCategory(numUnit);
19733
- const denCat = getUnitCategory(denUnit);
19734
- if (numCat !== denCat && numCat !== "other" && denCat !== "other") {
19735
- const massSide = numCat === "mass" ? strength.numerator : strength.denominator;
19736
- const volSide = numCat === "volume" ? strength.numerator : strength.denominator;
19737
- const bridgeDensity = massSide.value * getBaseUnitFactor(massSide.unit) / (volSide.value * getBaseUnitFactor(volSide.unit));
19738
- if (fCat === "mass") {
19739
- const valueMg = value * getBaseUnitFactor(fromUnit);
19740
- const valueMl = valueMg / bridgeDensity;
19741
- return valueMl / getBaseUnitFactor(toUnit);
19742
- } else {
19743
- const valueMl = value * getBaseUnitFactor(fromUnit);
19744
- const valueMg = valueMl * bridgeDensity;
19745
- return valueMg / getBaseUnitFactor(toUnit);
19746
- }
19747
- }
19748
- }
19749
- return null;
19750
- }
19751
-
19752
20015
  // src/utils/strength.ts
19753
20016
  function parseStrength(strength, context) {
19754
20017
  var _a2, _b, _c;
@@ -20188,6 +20451,24 @@ function estimateIngredientQuantity(quantity, context) {
20188
20451
  if (!(numerator == null ? void 0 : numerator.unit) || numerator.value === void 0 || !(denominator == null ? void 0 : denominator.unit) || denominator.value === void 0) {
20189
20452
  return void 0;
20190
20453
  }
20454
+ const quantityCategory = getUnitCategory(quantity.unit);
20455
+ const denominatorCategory = getUnitCategory(denominator.unit);
20456
+ if (quantityCategory !== "other" && quantityCategory === denominatorCategory) {
20457
+ const quantityInDenominatorBase = quantity.value * getBaseUnitFactor(quantity.unit);
20458
+ const denominatorInBase = denominator.value * getBaseUnitFactor(denominator.unit);
20459
+ const numeratorInBase = numerator.value * getBaseUnitFactor(numerator.unit);
20460
+ if (denominatorInBase !== 0) {
20461
+ return {
20462
+ value: roundCalculatedUnits(
20463
+ quantityInDenominatorBase * numeratorInBase / denominatorInBase / getBaseUnitFactor(numerator.unit)
20464
+ ),
20465
+ unit: numerator.unit,
20466
+ confidence: quantity.confidence,
20467
+ basis: quantity.basis,
20468
+ source: quantity.source
20469
+ };
20470
+ }
20471
+ }
20191
20472
  const converted = convertValue(
20192
20473
  quantity.value,
20193
20474
  quantity.unit,
package/dist/index.js CHANGED
@@ -2152,6 +2152,14 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2152
2152
  names: ["axilla", "axillae", "armpit", "armpits"],
2153
2153
  definition: { coding: { code: "34797008", display: "Axilla structure" }, routeHint: RouteCode["Topical route"] }
2154
2154
  },
2155
+ {
2156
+ names: ["axillary hair", "armpit hair", "armpit hairs", "underarm hair", "underarm hairs"],
2157
+ definition: {
2158
+ coding: { code: "75703003", display: "Structure of hair of axilla" },
2159
+ text: "axillary hair",
2160
+ routeHint: RouteCode["Topical route"]
2161
+ }
2162
+ },
2155
2163
  {
2156
2164
  names: ["groin"],
2157
2165
  definition: { coding: { code: "26893007", display: "Inguinal region structure" }, routeHint: RouteCode["Topical route"] }
@@ -2222,6 +2230,71 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2222
2230
  names: ["face"],
2223
2231
  definition: { coding: { code: "89545001", display: "Face" }, routeHint: RouteCode["Topical route"] }
2224
2232
  },
2233
+ {
2234
+ names: ["eyebrow", "brow"],
2235
+ definition: {
2236
+ coding: { code: "392262008", display: "Eyebrow structure" },
2237
+ text: "eyebrow",
2238
+ routeHint: RouteCode["Topical route"]
2239
+ }
2240
+ },
2241
+ {
2242
+ names: ["eyebrows", "brows"],
2243
+ definition: {
2244
+ coding: { code: "392262008", display: "Eyebrow structure" },
2245
+ text: "eyebrows",
2246
+ routeHint: RouteCode["Topical route"]
2247
+ }
2248
+ },
2249
+ {
2250
+ names: ["left eyebrow", "left brow"],
2251
+ definition: {
2252
+ coding: { code: "722011002", display: "Structure of eyebrow of left eye region" },
2253
+ text: "left eyebrow",
2254
+ routeHint: RouteCode["Topical route"]
2255
+ }
2256
+ },
2257
+ {
2258
+ names: ["right eyebrow", "right brow"],
2259
+ definition: {
2260
+ coding: { code: "722012009", display: "Structure of eyebrow of right eye region" },
2261
+ text: "right eyebrow",
2262
+ routeHint: RouteCode["Topical route"]
2263
+ }
2264
+ },
2265
+ {
2266
+ names: [
2267
+ "both eyebrows",
2268
+ "bilateral eyebrows",
2269
+ "each eyebrow",
2270
+ "left and right eyebrow",
2271
+ "left and right eyebrows",
2272
+ "right and left eyebrow",
2273
+ "right and left eyebrows",
2274
+ "left right eyebrow",
2275
+ "left right eyebrows"
2276
+ ],
2277
+ definition: {
2278
+ coding: {
2279
+ code: buildSnomedBodySiteLateralityPostcoordinationCode(
2280
+ "392262008",
2281
+ SNOMED_CT_BILATERAL_QUALIFIER_CODE
2282
+ ),
2283
+ display: "both eyebrows"
2284
+ },
2285
+ text: "both eyebrows",
2286
+ administrationTargetCount: 2,
2287
+ routeHint: RouteCode["Topical route"]
2288
+ }
2289
+ },
2290
+ {
2291
+ names: ["eyebrow hair", "eyebrow hairs", "brow hair", "brow hairs"],
2292
+ definition: {
2293
+ coding: { code: "392261001", display: "Hair structure of eyebrow" },
2294
+ text: "eyebrow hair",
2295
+ routeHint: RouteCode["Topical route"]
2296
+ }
2297
+ },
2225
2298
  {
2226
2299
  names: ["eyelid", "eyelids"],
2227
2300
  definition: { coding: { code: "80243003", display: "Eyelid" }, routeHint: RouteCode["Ophthalmic route"] }
@@ -2342,6 +2415,30 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2342
2415
  names: ["perineum"],
2343
2416
  definition: { coding: { code: "243990009", display: "Entire perineum" }, routeHint: RouteCode["Topical route"] }
2344
2417
  },
2418
+ {
2419
+ names: ["mustache", "moustache", "mustache hair", "moustache hair"],
2420
+ definition: {
2421
+ coding: { code: "256925006", display: "Structure of hair of mustache" },
2422
+ text: "mustache",
2423
+ routeHint: RouteCode["Topical route"]
2424
+ }
2425
+ },
2426
+ {
2427
+ names: ["beard", "beard hair", "facial hair"],
2428
+ definition: {
2429
+ coding: { code: "367576007", display: "Structure of beard hair" },
2430
+ text: "beard",
2431
+ routeHint: RouteCode["Topical route"]
2432
+ }
2433
+ },
2434
+ {
2435
+ names: ["pubic hair"],
2436
+ definition: {
2437
+ coding: { code: "75776007", display: "Structure of hair of pubis" },
2438
+ text: "pubic hair",
2439
+ routeHint: RouteCode["Topical route"]
2440
+ }
2441
+ },
2345
2442
  {
2346
2443
  names: ["skin"],
2347
2444
  definition: { coding: { code: "181469002", display: "Entire skin" }, routeHint: RouteCode["Topical route"] }
@@ -2352,6 +2449,38 @@ var DEFAULT_BODY_SITE_SNOMED_SOURCE = [
2352
2449
  coding: { code: "386045008", display: "Hair structure (body structure)" },
2353
2450
  routeHint: RouteCode["Topical route"]
2354
2451
  }
2452
+ },
2453
+ {
2454
+ names: ["joint"],
2455
+ definition: {
2456
+ coding: { code: "39352004", display: "Joint structure" },
2457
+ text: "joint",
2458
+ routeHint: RouteCode["Topical route"]
2459
+ }
2460
+ },
2461
+ {
2462
+ names: ["joints"],
2463
+ definition: {
2464
+ coding: { code: "81087007", display: "Joints" },
2465
+ text: "joints",
2466
+ routeHint: RouteCode["Topical route"]
2467
+ }
2468
+ },
2469
+ {
2470
+ names: ["finger joint", "finger joints"],
2471
+ definition: {
2472
+ coding: { code: "125682004", display: "Finger joint structure" },
2473
+ text: "finger joints",
2474
+ routeHint: RouteCode["Topical route"]
2475
+ }
2476
+ },
2477
+ {
2478
+ names: ["knuckle", "knuckles"],
2479
+ definition: {
2480
+ coding: { code: "70420003", display: "Metacarpophalangeal joint structure" },
2481
+ text: "knuckles",
2482
+ routeHint: RouteCode["Topical route"]
2483
+ }
2355
2484
  }
2356
2485
  ];
2357
2486
  var DEFAULT_BODY_SITE_SNOMED = objectFromEntries(
@@ -4263,6 +4392,7 @@ var lexical_classes_default = {
4263
4392
  "Use shampoo": "\u0E2A\u0E23\u0E30"
4264
4393
  },
4265
4394
  compoundDoseUnits: [
4395
+ { head: "cm", tails: [], requiresSiteContext: true, unit: "cm line" },
4266
4396
  { head: "cm", tails: ["ribbon", "ribbons"], unit: "cm ribbon" },
4267
4397
  { head: "cm", tails: ["strip", "strips"], unit: "cm strip" },
4268
4398
  { head: "cm", tails: ["line", "lines"], unit: "cm line" },
@@ -4271,6 +4401,8 @@ var lexical_classes_default = {
4271
4401
  { head: "fingertip", tails: ["unit", "units"], unit: "FTU" },
4272
4402
  { head: "eye", tails: ["drop", "drops"], unit: "drop" },
4273
4403
  { head: "pea-sized", tails: ["amount"], unit: "pea-sized amount" },
4404
+ { head: "pea-size", tails: ["amount"], unit: "pea-sized amount" },
4405
+ { head: "peasize", tails: [], tailSequences: [[]], unit: "pea-sized amount" },
4274
4406
  { head: "\u0E40\u0E21\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27", tails: [], unit: "pea-sized amount" },
4275
4407
  { head: "\u0E40\u0E21\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27\u0E40\u0E02\u0E35\u0E22\u0E27", tails: [], unit: "pea-sized amount" },
4276
4408
  { head: "\u0E40\u0E21\u0E25\u0E47\u0E14\u0E16\u0E31\u0E48\u0E27", tails: [], unit: "pea-sized amount" },
@@ -4280,7 +4412,7 @@ var lexical_classes_default = {
4280
4412
  {
4281
4413
  head: "pea",
4282
4414
  tails: [],
4283
- tailSequences: [["sized", "amount"], ["size", "amount"]],
4415
+ tailSequences: [["sized", "amount"], ["size", "amount"], ["amount"], ["sized"], ["size"], []],
4284
4416
  unit: "pea-sized amount"
4285
4417
  },
4286
4418
  { head: "hand", tails: ["print", "prints"], unit: "handprint" },
@@ -14926,6 +15058,11 @@ var unit_terminology_default = {
14926
15058
  unit: "pea-sized amount",
14927
15059
  kind: "product_specific_amount",
14928
15060
  aliases: [
15061
+ "pea",
15062
+ "pea amount",
15063
+ "peasize",
15064
+ "pea-size",
15065
+ "pea size",
14929
15066
  "pea sized amount",
14930
15067
  "pea-sized",
14931
15068
  "pea sized",
@@ -15022,10 +15159,96 @@ var unit_terminology_default = {
15022
15159
  ]
15023
15160
  };
15024
15161
 
15162
+ // src/utils/units.ts
15163
+ var MASS_UNITS = {
15164
+ kg: 1e6,
15165
+ g: 1e3,
15166
+ mg: 1,
15167
+ mcg: 1e-3,
15168
+ ug: 1e-3,
15169
+ microg: 1e-3,
15170
+ ng: 1e-6
15171
+ };
15172
+ var VOLUME_UNITS = {
15173
+ l: 1e3,
15174
+ dl: 100,
15175
+ ml: 1,
15176
+ ul: 1e-3,
15177
+ microl: 1e-3,
15178
+ cm3: 1,
15179
+ tsp: 5,
15180
+ tbsp: 15
15181
+ };
15182
+ function getUnitCategory(unit) {
15183
+ if (!unit) return "other";
15184
+ const u = unit.toLowerCase();
15185
+ if (MASS_UNITS[u] !== void 0) return "mass";
15186
+ if (VOLUME_UNITS[u] !== void 0) return "volume";
15187
+ return "other";
15188
+ }
15189
+ function getBaseUnitFactor(unit) {
15190
+ var _a2, _b;
15191
+ if (!unit) return 1;
15192
+ const u = unit.toLowerCase();
15193
+ return (_b = (_a2 = MASS_UNITS[u]) != null ? _a2 : VOLUME_UNITS[u]) != null ? _b : 1;
15194
+ }
15195
+ function convertValue(value, fromUnit, toUnit, strength) {
15196
+ const f = fromUnit.toLowerCase();
15197
+ const t = toUnit.toLowerCase();
15198
+ if (f === t) return value;
15199
+ const fCat = getUnitCategory(f);
15200
+ const tCat = getUnitCategory(t);
15201
+ if (fCat === tCat && fCat !== "other") {
15202
+ const fFactor = getBaseUnitFactor(f);
15203
+ const tFactor = getBaseUnitFactor(t);
15204
+ return value * fFactor / tFactor;
15205
+ }
15206
+ if (strength && (fCat === "mass" && tCat === "volume" || fCat === "volume" && tCat === "mass")) {
15207
+ const numUnit = strength.numerator.unit.toLowerCase();
15208
+ const denUnit = strength.denominator.unit.toLowerCase();
15209
+ const numCat = getUnitCategory(numUnit);
15210
+ const denCat = getUnitCategory(denUnit);
15211
+ if (numCat !== denCat && numCat !== "other" && denCat !== "other") {
15212
+ const massSide = numCat === "mass" ? strength.numerator : strength.denominator;
15213
+ const volSide = numCat === "volume" ? strength.numerator : strength.denominator;
15214
+ const bridgeDensity = massSide.value * getBaseUnitFactor(massSide.unit) / (volSide.value * getBaseUnitFactor(volSide.unit));
15215
+ if (fCat === "mass") {
15216
+ const valueMg = value * getBaseUnitFactor(fromUnit);
15217
+ const valueMl = valueMg / bridgeDensity;
15218
+ return valueMl / getBaseUnitFactor(toUnit);
15219
+ } else {
15220
+ const valueMl = value * getBaseUnitFactor(fromUnit);
15221
+ const valueMg = valueMl * bridgeDensity;
15222
+ return valueMg / getBaseUnitFactor(toUnit);
15223
+ }
15224
+ }
15225
+ }
15226
+ return null;
15227
+ }
15228
+
15025
15229
  // src/unit-lexicon.ts
15026
15230
  var HOUSEHOLD_VOLUME_UNIT_SET = new Set(
15027
15231
  HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase())
15028
15232
  );
15233
+ var MASS_DISPENSED_SEMISOLID_DOSAGE_FORMS = /* @__PURE__ */ new Set([
15234
+ "cream",
15235
+ "ointment",
15236
+ "gel",
15237
+ "paste",
15238
+ "cutaneous paste",
15239
+ "vaginal cream",
15240
+ "vaginal gel",
15241
+ "oral gel",
15242
+ "oral paste",
15243
+ "oromucosal gel",
15244
+ "oromucosal paste",
15245
+ "dental gel",
15246
+ "dental paste",
15247
+ "gingival gel",
15248
+ "nasal gel",
15249
+ "eye gel",
15250
+ "eye ointment"
15251
+ ]);
15029
15252
  var DOSE_UNIT_TERMINOLOGY = unit_terminology_default.terms;
15030
15253
  var DOSE_UNIT_TERMINOLOGY_BY_KEY = /* @__PURE__ */ new Map();
15031
15254
  var DISCRETE_UNIT_KINDS = /* @__PURE__ */ new Set([
@@ -15088,6 +15311,47 @@ function unitApproximationOverride(unit, context) {
15088
15311
  }
15089
15312
  return void 0;
15090
15313
  }
15314
+ function normalizeDosageFormKey(form) {
15315
+ var _a2;
15316
+ const normalized = form == null ? void 0 : form.trim().toLowerCase();
15317
+ if (!normalized) {
15318
+ return void 0;
15319
+ }
15320
+ return (_a2 = KNOWN_DOSAGE_FORMS_TO_DOSE[normalized]) != null ? _a2 : normalized;
15321
+ }
15322
+ function getPreferredMassApproximationUnit(context) {
15323
+ var _a2;
15324
+ const normalizedDosageForm = normalizeDosageFormKey(context == null ? void 0 : context.dosageForm);
15325
+ if (!normalizedDosageForm || !MASS_DISPENSED_SEMISOLID_DOSAGE_FORMS.has(normalizedDosageForm)) {
15326
+ return void 0;
15327
+ }
15328
+ const containerUnit = (_a2 = context == null ? void 0 : context.containerUnit) == null ? void 0 : _a2.trim();
15329
+ if (containerUnit && getUnitCategory(containerUnit) === "mass") {
15330
+ return containerUnit;
15331
+ }
15332
+ const defaultUnit = normalizedDosageForm ? DEFAULT_UNIT_BY_NORMALIZED_FORM[normalizedDosageForm] : void 0;
15333
+ return defaultUnit && getUnitCategory(defaultUnit) === "mass" ? defaultUnit : void 0;
15334
+ }
15335
+ function bridgeApproximationToMassDispensedTopical(approximation, context) {
15336
+ const preferredMassUnit = getPreferredMassApproximationUnit(context);
15337
+ if (!preferredMassUnit || getUnitCategory(approximation.unit) !== "volume") {
15338
+ return approximation;
15339
+ }
15340
+ const sourceVolumeFactor = VOLUME_UNITS[approximation.unit.toLowerCase()];
15341
+ const targetMassFactor = MASS_UNITS[preferredMassUnit.toLowerCase()];
15342
+ if (!sourceVolumeFactor || !targetMassFactor) {
15343
+ return approximation;
15344
+ }
15345
+ const valueMl = approximation.value * sourceVolumeFactor;
15346
+ const valueG = valueMl;
15347
+ const convertedValue = valueG * MASS_UNITS.g / targetMassFactor;
15348
+ const bridgeBasis = "Mass-dispensed semisolid topical default bridge assumes 1 mL approximately equals 1 g unless a product-specific override is provided";
15349
+ return __spreadProps(__spreadValues({}, approximation), {
15350
+ value: convertedValue,
15351
+ unit: preferredMassUnit,
15352
+ basis: approximation.basis ? `${approximation.basis}; ${bridgeBasis}` : bridgeBasis
15353
+ });
15354
+ }
15091
15355
  function getDoseUnitTerminologyEntry(unit) {
15092
15356
  var _a2;
15093
15357
  if (!unit) {
@@ -15109,7 +15373,11 @@ function getDoseUnitApproximation(unit, context) {
15109
15373
  if (override) {
15110
15374
  return override;
15111
15375
  }
15112
- return terminologyEntry == null ? void 0 : terminologyEntry.approximateQuantity;
15376
+ const approximation = terminologyEntry == null ? void 0 : terminologyEntry.approximateQuantity;
15377
+ if (!approximation) {
15378
+ return void 0;
15379
+ }
15380
+ return bridgeApproximationToMassDispensedTopical(approximation, context);
15113
15381
  }
15114
15382
  function getDoseUnitSemantics(unit, context) {
15115
15383
  if (!unit) {
@@ -16356,6 +16624,64 @@ function productLexicalRule() {
16356
16624
  }
16357
16625
  function matchCompoundDoseUnit(context, start, lower) {
16358
16626
  var _a2;
16627
+ const MAX_SITE_CONTEXT_TOKENS = 3;
16628
+ const hasSiteMeaning = (lowerValue) => {
16629
+ var _a3, _b, _c;
16630
+ return Boolean(
16631
+ lowerValue && (DEFAULT_BODY_SITE_SNOMED[normalizeBodySiteKey(lowerValue)] || resolveBodySitePhrase(lowerValue, (_a3 = context.options) == null ? void 0 : _a3.siteCodeMap, {
16632
+ bodySiteContext: (_c = (_b = context.options) == null ? void 0 : _b.context) == null ? void 0 : _c.bodySiteContext
16633
+ }))
16634
+ );
16635
+ };
16636
+ const collectForwardSitePhrases = (phraseStart) => {
16637
+ const parts = [];
16638
+ const phrases = [];
16639
+ for (let index = phraseStart; index < context.limit && parts.length < MAX_SITE_CONTEXT_TOKENS; index += 1) {
16640
+ const token = context.tokens[index];
16641
+ if (!token || context.state.consumed.has(token.index)) {
16642
+ break;
16643
+ }
16644
+ const lowerValue = normalizeTokenLower(token);
16645
+ if (!lowerValue || isPunctuation(lowerValue)) {
16646
+ break;
16647
+ }
16648
+ parts.push(lowerValue);
16649
+ phrases.push(parts.join(" "));
16650
+ }
16651
+ return phrases;
16652
+ };
16653
+ const matchesSiteContext = () => {
16654
+ const next = context.tokens[start + 1];
16655
+ const nextLower = next && !context.state.consumed.has(next.index) ? normalizeTokenLower(next) : void 0;
16656
+ if (nextLower && (ROUTE_SITE_PREPOSITIONS.has(nextLower) || SITE_ANCHORS.has(nextLower))) {
16657
+ const followingSitePhrases = collectForwardSitePhrases(start + 2);
16658
+ if (followingSitePhrases.some((phrase) => hasSiteMeaning(phrase))) {
16659
+ return true;
16660
+ }
16661
+ }
16662
+ const previous = context.tokens[start - 1];
16663
+ const previousLower = previous && !context.state.consumed.has(previous.index) ? normalizeTokenLower(previous) : void 0;
16664
+ const trailingNumberBeforeUnit = Boolean(
16665
+ previousLower && /^[0-9]+(?:\.[0-9]+)?$/.test(previousLower)
16666
+ );
16667
+ const siteEnd = start - (trailingNumberBeforeUnit ? 2 : 1);
16668
+ for (let phraseLength = 1; phraseLength <= MAX_SITE_CONTEXT_TOKENS; phraseLength += 1) {
16669
+ const phraseStart = siteEnd - phraseLength + 1;
16670
+ if (phraseStart < 0) {
16671
+ break;
16672
+ }
16673
+ const precedingAnchor = context.tokens[phraseStart - 1];
16674
+ const precedingAnchorLower = precedingAnchor && !context.state.consumed.has(precedingAnchor.index) ? normalizeTokenLower(precedingAnchor) : void 0;
16675
+ if (!precedingAnchorLower || !ROUTE_SITE_PREPOSITIONS.has(precedingAnchorLower) && !SITE_ANCHORS.has(precedingAnchorLower)) {
16676
+ continue;
16677
+ }
16678
+ const phrase = collectForwardSitePhrases(phraseStart)[phraseLength - 1];
16679
+ if (hasSiteMeaning(phrase)) {
16680
+ return true;
16681
+ }
16682
+ }
16683
+ return false;
16684
+ };
16359
16685
  for (const compound of COMPOUND_DOSE_UNITS) {
16360
16686
  if (compound.head !== lower) {
16361
16687
  continue;
@@ -16385,6 +16711,10 @@ function matchCompoundDoseUnit(context, start, lower) {
16385
16711
  return head ? { unit: compound.unit, tokens: [head, next] } : void 0;
16386
16712
  }
16387
16713
  }
16714
+ if (compound.requiresSiteContext && matchesSiteContext()) {
16715
+ const head = context.tokens[start];
16716
+ return head ? { unit: compound.unit, tokens: [head] } : void 0;
16717
+ }
16388
16718
  }
16389
16719
  return void 0;
16390
16720
  }
@@ -19582,73 +19912,6 @@ function suggestSig(input, options) {
19582
19912
  );
19583
19913
  }
19584
19914
 
19585
- // src/utils/units.ts
19586
- var MASS_UNITS = {
19587
- kg: 1e6,
19588
- g: 1e3,
19589
- mg: 1,
19590
- mcg: 1e-3,
19591
- ug: 1e-3,
19592
- microg: 1e-3,
19593
- ng: 1e-6
19594
- };
19595
- var VOLUME_UNITS = {
19596
- l: 1e3,
19597
- dl: 100,
19598
- ml: 1,
19599
- ul: 1e-3,
19600
- microl: 1e-3,
19601
- cm3: 1,
19602
- tsp: 5,
19603
- tbsp: 15
19604
- };
19605
- function getUnitCategory(unit) {
19606
- if (!unit) return "other";
19607
- const u = unit.toLowerCase();
19608
- if (MASS_UNITS[u] !== void 0) return "mass";
19609
- if (VOLUME_UNITS[u] !== void 0) return "volume";
19610
- return "other";
19611
- }
19612
- function getBaseUnitFactor(unit) {
19613
- var _a2, _b;
19614
- if (!unit) return 1;
19615
- const u = unit.toLowerCase();
19616
- return (_b = (_a2 = MASS_UNITS[u]) != null ? _a2 : VOLUME_UNITS[u]) != null ? _b : 1;
19617
- }
19618
- function convertValue(value, fromUnit, toUnit, strength) {
19619
- const f = fromUnit.toLowerCase();
19620
- const t = toUnit.toLowerCase();
19621
- if (f === t) return value;
19622
- const fCat = getUnitCategory(f);
19623
- const tCat = getUnitCategory(t);
19624
- if (fCat === tCat && fCat !== "other") {
19625
- const fFactor = getBaseUnitFactor(f);
19626
- const tFactor = getBaseUnitFactor(t);
19627
- return value * fFactor / tFactor;
19628
- }
19629
- if (strength && (fCat === "mass" && tCat === "volume" || fCat === "volume" && tCat === "mass")) {
19630
- const numUnit = strength.numerator.unit.toLowerCase();
19631
- const denUnit = strength.denominator.unit.toLowerCase();
19632
- const numCat = getUnitCategory(numUnit);
19633
- const denCat = getUnitCategory(denUnit);
19634
- if (numCat !== denCat && numCat !== "other" && denCat !== "other") {
19635
- const massSide = numCat === "mass" ? strength.numerator : strength.denominator;
19636
- const volSide = numCat === "volume" ? strength.numerator : strength.denominator;
19637
- const bridgeDensity = massSide.value * getBaseUnitFactor(massSide.unit) / (volSide.value * getBaseUnitFactor(volSide.unit));
19638
- if (fCat === "mass") {
19639
- const valueMg = value * getBaseUnitFactor(fromUnit);
19640
- const valueMl = valueMg / bridgeDensity;
19641
- return valueMl / getBaseUnitFactor(toUnit);
19642
- } else {
19643
- const valueMl = value * getBaseUnitFactor(fromUnit);
19644
- const valueMg = valueMl * bridgeDensity;
19645
- return valueMg / getBaseUnitFactor(toUnit);
19646
- }
19647
- }
19648
- }
19649
- return null;
19650
- }
19651
-
19652
19915
  // src/utils/strength.ts
19653
19916
  function parseStrength(strength, context) {
19654
19917
  var _a2, _b, _c;
@@ -20088,6 +20351,24 @@ function estimateIngredientQuantity(quantity, context) {
20088
20351
  if (!(numerator == null ? void 0 : numerator.unit) || numerator.value === void 0 || !(denominator == null ? void 0 : denominator.unit) || denominator.value === void 0) {
20089
20352
  return void 0;
20090
20353
  }
20354
+ const quantityCategory = getUnitCategory(quantity.unit);
20355
+ const denominatorCategory = getUnitCategory(denominator.unit);
20356
+ if (quantityCategory !== "other" && quantityCategory === denominatorCategory) {
20357
+ const quantityInDenominatorBase = quantity.value * getBaseUnitFactor(quantity.unit);
20358
+ const denominatorInBase = denominator.value * getBaseUnitFactor(denominator.unit);
20359
+ const numeratorInBase = numerator.value * getBaseUnitFactor(numerator.unit);
20360
+ if (denominatorInBase !== 0) {
20361
+ return {
20362
+ value: roundCalculatedUnits(
20363
+ quantityInDenominatorBase * numeratorInBase / denominatorInBase / getBaseUnitFactor(numerator.unit)
20364
+ ),
20365
+ unit: numerator.unit,
20366
+ confidence: quantity.confidence,
20367
+ basis: quantity.basis,
20368
+ source: quantity.source
20369
+ };
20370
+ }
20371
+ }
20091
20372
  const converted = convertValue(
20092
20373
  quantity.value,
20093
20374
  quantity.unit,
package/dist/types.d.ts CHANGED
@@ -301,9 +301,14 @@ export declare enum FhirDayOfWeek {
301
301
  Saturday = "sat",
302
302
  Sunday = "sun"
303
303
  }
304
+ export interface FhirPeriod {
305
+ start?: string;
306
+ end?: string;
307
+ }
304
308
  export interface FhirTimingRepeat {
305
309
  count?: number;
306
310
  boundsDuration?: FhirQuantity;
311
+ boundsPeriod?: FhirPeriod;
307
312
  boundsRange?: FhirRange;
308
313
  frequency?: number;
309
314
  frequencyMax?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",