50 lines
2.5 KiB
TypeScript
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;
|
|
}
|