Files
helios/server/src/lib/units.ts

50 lines
2.5 KiB
TypeScript

// Lab unit conversion. Two transform kinds: dimensional scaling (prefix/volume,
// analyte-independent) and the mass<->mole bridge, which requires the analyte's
// molar mass. %, ratios, IU, and cell counts never bridge to mass or mole.
type Kind = "mass" | "mole" | "count" | "iu" | "percent" | "ratio" | "other";
const AMOUNT: Record<string, [Kind, number]> = {
g: ["mass", 1], mg: ["mass", 1e-3], ug: ["mass", 1e-6], mcg: ["mass", 1e-6],
"µg": ["mass", 1e-6], ng: ["mass", 1e-9], pg: ["mass", 1e-12],
mol: ["mole", 1], mmol: ["mole", 1e-3], umol: ["mole", 1e-6], "µmol": ["mole", 1e-6],
nmol: ["mole", 1e-9], pmol: ["mole", 1e-12],
cells: ["count", 1], k: ["count", 1e3], thousand: ["count", 1e3],
million: ["count", 1e6], "x10^9": ["count", 1e9], "x10^12": ["count", 1e12],
iu: ["iu", 1], miu: ["iu", 1e-3], uiu: ["iu", 1e-6], "µiu": ["iu", 1e-6], u: ["iu", 1],
};
const VOLUME: Record<string, number> = { l: 1, dl: 0.1, ml: 1e-3, ul: 1e-6, "µl": 1e-6 };
const clean = (u: string) => u.toLowerCase().replace(/\s+/g, "").replace("μ", "µ");
interface Parsed { kind: Kind; amountFactor: number; volumeFactor: number }
export function parseUnit(unit: string): Parsed | null {
const u = clean(unit);
if (u === "%") return { kind: "percent", amountFactor: 1, volumeFactor: 1 };
if (u === "" || u === "ratio" || u === "index") return { kind: "ratio", amountFactor: 1, volumeFactor: 1 };
const m = u.match(/^([a-zµ0-9^]+)\/([a-zµ0-9.]+)$/);
if (m) {
const amtEntry = AMOUNT[m[1]];
const volFactor = VOLUME[m[2]];
if (amtEntry && volFactor) return { kind: amtEntry[0], amountFactor: amtEntry[1], volumeFactor: volFactor };
}
return { kind: "other", amountFactor: 1, volumeFactor: 1 };
}
export function convert(value: number, fromUnit: string, toUnit: string, molarMass?: number | null): number | null {
const from = parseUnit(fromUnit);
const to = parseUnit(toUnit);
if (!from || !to) return null;
if (clean(fromUnit) === clean(toUnit)) return value;
if (from.kind === "other" || to.kind === "other") return null;
const fromBasePerL = (value * from.amountFactor) / from.volumeFactor;
let toBasePerL = fromBasePerL;
if (from.kind !== to.kind) {
const bridge = (from.kind === "mass" && to.kind === "mole") || (from.kind === "mole" && to.kind === "mass");
if (!bridge || !molarMass) return null;
toBasePerL = from.kind === "mass" ? fromBasePerL / molarMass : fromBasePerL * molarMass;
}
return (toBasePerL * to.volumeFactor) / to.amountFactor;
}