Files
HealthLog/src/lib/labs/unit-conversion/atomic-weights.ts
T
BernhardBuckel 503f0f183a feat(labs): add the lab unit conversion registry and its pure functions
Phase 2 of #1095: new code only, no behaviour change. Nothing imports it yet.

An analyte lists its units as affine maps to a hub unit (hub = a * value + b),
so any pair goes unit -> hub -> unit. Molar-mass factors are derived from the
formula and a cited atomic-weight table instead of being typed; glucose reuses
MGDL_PER_MMOL. HbA1c is the IFCC-NGSP master equation. Units that differ only
by a power of ten convert for any analyte, inside one family and never across
mass and amount. An entry with no source, a hub that is not one of its units, or
a non-positive factor makes the registry fail to load.

convertLabValue, convertLabRange and conversionsFor refuse rather than guess:
unknown unit, unknown analyte, a mass to amount pair without an analyte key,
a value that is not finite. Triglycerides, haemoglobin in mmol/L, ferritin and
B12 in pmol/L and enzymes in ukat/L stay refusals.

Refs #1095
2026-10-04 11:05:36 +02:00

51 lines
1.7 KiB
TypeScript

/**
* The atomic weights the molar-mass conversions are derived from.
*
* Conventional atomic weights as published by the IUPAC Commission on Isotopic
* Abundances and Atomic Weights (CIAAW), "Standard atomic weights". Only the
* elements a conversion in the registry needs are listed; one more element is
* one more line here, with a conversion that uses it.
*
* A molar mass is summed from these, never typed, so a long decimal cannot be
* copied wrong into a factor that then touches every reading of a marker.
*/
export const ATOMIC_WEIGHTS_SOURCE =
"IUPAC CIAAW, Standard atomic weights (conventional values)";
export const ATOMIC_WEIGHTS = {
H: 1.008,
C: 12.011,
N: 14.007,
O: 15.999,
Ca: 40.078,
} as const;
export type Element = keyof typeof ATOMIC_WEIGHTS;
/** Atoms per molecule, by element: urea is `{ C: 1, H: 4, N: 2, O: 1 }`. */
export type Formula = Readonly<Partial<Record<Element, number>>>;
/** g/mol of a formula, summed from `ATOMIC_WEIGHTS`. */
export function molarMass(formula: Formula): number {
let total = 0;
for (const [element, count] of Object.entries(formula) as Array<
[Element, number]
>) {
total += ATOMIC_WEIGHTS[element] * count;
}
// Summing decimals leaves float noise past the sixth place (180.15600000000001).
return Math.round(total * 1e6) / 1e6;
}
/** `C6H12O6` for `{ C: 6, H: 12, O: 6 }`: for a citation, in a stable order. */
export function formulaLabel(formula: Formula): string {
const order: Element[] = ["C", "H", "N", "O", "Ca"];
return order
.filter((element) => (formula[element] ?? 0) > 0)
.map((element) => {
const count = formula[element]!;
return count === 1 ? element : `${element}${count}`;
})
.join("");
}