feat(labs): unit conversion, analyte registry, marker normalization
This commit is contained in:
49
server/src/lib/units.ts
Normal file
49
server/src/lib/units.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user