From 060453ad806dbb0a198029d98bdd8ac52434febf Mon Sep 17 00:00:00 2001 From: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> Date: Sun, 8 Nov 2026 15:02:33 -0600 Subject: [PATCH] Fix factsheet dose chart unit comparisons --- src/components/DrugInfo.tsx | 101 ++++++------------- src/utils/__tests__/parseDoseRangeMg.test.ts | 24 +++++ src/utils/parseDoseRangeMg.ts | 28 +++++ 3 files changed, 83 insertions(+), 70 deletions(-) create mode 100644 src/utils/__tests__/parseDoseRangeMg.test.ts create mode 100644 src/utils/parseDoseRangeMg.ts diff --git a/src/components/DrugInfo.tsx b/src/components/DrugInfo.tsx index 3ab65a0..490d8c4 100644 --- a/src/components/DrugInfo.tsx +++ b/src/components/DrugInfo.tsx @@ -12,6 +12,7 @@ import type { ApexAxisChartSeries } from "apexcharts"; import dynamic from "next/dynamic"; import comboDefinitions from "../assets/comboDefinitions.json"; import addDictionaryDefs from "./addDictionaryDefs"; +import { parseDoseRangeMg } from "../utils/parseDoseRangeMg"; // We need to dynamically load this module const ReactApexChart = dynamic(() => import("react-apexcharts"), { @@ -454,71 +455,23 @@ const addDosages = (drugData: Drug) => { [strength]: strengthData, }; - // This essentially looks for "(any digit) (optional dash) (any digit) (optional unit)" - const regex = /(\d+(?:\.\d+)?)(?:-(\d+(?:\.\d+)?))?([a-zA-Z]+)?/; - const match = RegExp(regex).exec(strengthData); - - // This is mostly for type-safety - if (match) { - // The minimum number is the first number to appear - const strengthMinNumber = parseFloat(match[1]); - - // The maximum number is either the second number, or if there is no second number, the first number * 1.5 - // Later, we have code that says "if there is no maximum, then do a fade out", the "1.5" is purely cosmetic - // The doseText above displays the "+" so it's clear to the user, they never see the (dosage * 1.5) value - const strengthMaxNumber = match[2] - ? parseFloat(match[2]) - : strengthMinNumber * 1.5; - - // Get the color associated with the strength - const strengthColor = - doseColorData[strength as keyof typeof doseColorData]; - // If it's not already in the doseColors table, add it - if (!doseColors.includes(strengthColor)) { - doseColors.push(strengthColor); - } - - // Get the gradient color associated with the strength - // If it's not already in the doseGradientToColors table, add it - const strengthGradientColor = match[2] - ? doseGradientData[strength as keyof typeof doseGradientData] - : "#ffffff"; - if (!doseGradientToColors.includes(strengthGradientColor)) { - doseGradientToColors.push(strengthGradientColor); - } - - // Check if the doseSeries already has a series with the same name - // If it does, push the new data to that series - // If it doesn't, create a new series - const existingSeries = doseSeries.find( - (series) => series.name === strength, - ); + const range = parseDoseRangeMg(strengthData); + if (!range) return; - const roaUnit = match[3] ? ` (${match[3]})` : ``; - if (existingSeries) { - const existingSeriesData = existingSeries.data as { - x: string; - y: number[]; - }[]; - - existingSeriesData.push({ - x: `${roa}${roaUnit}`, - y: [strengthMinNumber, strengthMaxNumber], - }); - return; - } - - const dosePropertySeries = { - name: strength, - data: [ - { - x: `${roa}${roaUnit}`, - y: [strengthMinNumber, strengthMaxNumber], - }, - ], - }; - doseSeries.push(dosePropertySeries); + const point = { x: `${roa} (mg)`, y: range }; + const existingSeries = doseSeries.find( + (series) => series.name === strength, + ); + if (existingSeries) { + (existingSeries.data as (typeof point)[]).push(point); + return; } + + doseColors.push(doseColorData[strength as keyof typeof doseColorData]); + doseGradientToColors.push( + doseGradientData[strength as keyof typeof doseGradientData], + ); + doseSeries.push({ name: strength, data: [point] }); }); }); @@ -594,13 +547,21 @@ const addDosages = (drugData: Drug) => { Dosages - + {doseSeries.length > 0 && ( + <> + + Chart shows bounded mass ranges in mg. See the table for all + doses. + + + + )} {/* Dosage Text */} diff --git a/src/utils/__tests__/parseDoseRangeMg.test.ts b/src/utils/__tests__/parseDoseRangeMg.test.ts new file mode 100644 index 0000000..7704787 --- /dev/null +++ b/src/utils/__tests__/parseDoseRangeMg.test.ts @@ -0,0 +1,24 @@ +import { parseDoseRangeMg } from "../parseDoseRangeMg"; + +describe("parseDoseRangeMg", () => { + it.each([ + ["800-1500ug", [0.8, 1.5]], + ["1.5-3mg", [1.5, 3]], + ["325mg-650mg", [325, 650]], + ["800 µg – 1.5 mg", [0.8, 1.5]], + ])("normalizes %s to milligrams", (source, expected) => { + expect(parseDoseRangeMg(source)).toEqual(expected); + }); + + it.each([ + "25-50ug/hr", + "25ug/hr-50ug/hr", + "3mg+", + "3mg", + "1-3", + "3mg-1mg", + "1-3mg/kg", + ])("leaves %s in the text table only", (source) => + expect(parseDoseRangeMg(source)).toBeUndefined(), + ); +}); diff --git a/src/utils/parseDoseRangeMg.ts b/src/utils/parseDoseRangeMg.ts new file mode 100644 index 0000000..53f0e25 --- /dev/null +++ b/src/utils/parseDoseRangeMg.ts @@ -0,0 +1,28 @@ +const massInMg = { ng: 0.000001, ug: 0.001, mg: 1, g: 1000 }; +type MassUnit = keyof typeof massInMg; + +export function parseDoseRangeMg(text: string): [number, number] | undefined { + // Require two explicit endpoints and at least one mass unit. + const endpoints = text + .toLowerCase() + .replace(/[\u00b5\u03bc]/gu, "u") + .split(/[-\u2013\u2014]/u); + if (endpoints.length !== 2) return; + + const quantity = /^(\d+(?:\.\d+)?|\.\d+)\s*(ng|ug|mg|g)?$/u; + const lower = quantity.exec(endpoints[0].trim()); + const upper = quantity.exec(endpoints[1].trim()); + if (!lower || !upper) return; + + const [, low, lowUnit] = lower; + const [, high, highUnit] = upper; + if (!lowUnit && !highUnit) return; + + const startUnit = (lowUnit || highUnit) as MassUnit; + const endUnit = (highUnit || lowUnit) as MassUnit; + const start = Number(low) * massInMg[startUnit]; + const end = Number(high) * massInMg[endUnit]; + if (Number.isFinite(start) && Number.isFinite(end) && start < end) { + return [start, end]; + } +}