// Common laboratory tests under stable MARMAR codes: the names labs print for
// them, a representative LOINC code, and the unit results are charted in.
//
// The catalogue links results of the same test across labs (L6 charts, the
// check engine) and converts units where the conversion is a fixed formula.
// It never changes a printed value: a conversion only fills the optional
// `normalized` field. It holds no thresholds; interpreting a result is not
// done here.
export interface LabUnit {
/** How the unit is shown, e.g. "µmol/L". */
unit: string;
/** Other ways labs print it. Compared after unitKey(). */
aliases?: readonly string[];
/** This unit to the test's canonical unit. Absent for the canonical unit itself. */
toCanonical?: (value: number) => number;
}
export interface LabTest {
code: string;
name: string;
/** Representative LOINC code for results in the canonical unit. */
loinc: string;
/** Canonical unit; null for tests without one, such as INR. */
unit: string | null;
/** Accepted units, canonical first. */
units: readonly LabUnit[];
/** Printed names, compared after nameKey(). */
synonyms: readonly string[];
/** Decimal places for a converted value. */
decimals: number;
/** Every unit labs use for this test gives the same number, so a result
* printed without a unit can still be charted. */
unitOptional?: boolean;
}
const SAME = undefined;
const times = (factor: number) => (value: number) => value * factor;
const dividedBy = (divisor: number) => (value: number) => value / divisor;
const MMOL_PER_L = { unit: 'mmol/L', aliases: ['mmol/l', 'mmol per l', 'mmol/litre', 'mmol/liter'] } as const;
const MEQ_PER_L = { unit: 'mEq/L', aliases: ['meq/l'], toCanonical: SAME } as const;
const MG_PER_DL = ['mg/dl', 'mg/100ml', 'mg%', 'mg per dl'] as const;
const UNITS_PER_L = { unit: 'U/L', aliases: ['iu/l', 'u/l', 'units/l', 'unit/l', 'iu per l', 'u per l'] } as const;
// Factors are the standard clinical-chemistry ones: glucose 18.016 mg/dL per
// mmol/L, cholesterol 38.67, triglycerides 88.57, creatinine 88.42 µmol/L per
// mg/dL, vitamin B12 1.3554 ng/L per pmol/L, ACR 8.84 mg/g per mg/mmol, and
// the IFCC-NGSP master equation for HbA1c.
export const LAB_TESTS: readonly LabTest[] = [
{
code: 'egfr', name: 'eGFR', loinc: '62238-1', unit: 'mL/min/1.73m²', decimals: 0, unitOptional: true,
units: [{ unit: 'mL/min/1.73m²', aliases: ['ml/min/1.73m2', 'ml/min/1.73 m2', 'ml/min/1.73sqm', 'ml/min/1.73 sq m',
'ml/min/{1.73_m2}', 'ml/min per 1.73m2', 'ml/min/1.73', 'ml/min', 'ml/min/1.73m^2'] }],
synonyms: ['egfr', 'e gfr', 'gfr', 'estimated gfr', 'gfr estimated', 'estimated glomerular filtration rate',
'glomerular filtration rate', 'egfr ckd epi', 'ckd epi egfr', 'egfr mdrd', 'mdrd egfr'],
},
{
code: 'creatinine', name: 'Creatinine', loinc: '14682-9', unit: 'µmol/L', decimals: 0,
units: [
{ unit: 'µmol/L', aliases: ['umol/l', 'micromol/l', 'µmol/l', 'μmol/l', 'umol per l'] },
{ unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: times(88.42) },
{ unit: 'mmol/L', aliases: ['mmol/l'], toCanonical: times(1000) },
],
synonyms: ['creatinine', 'creat', 'creatinine level', 'serum creatinine', 'plasma creatinine'],
},
{
code: 'hba1c', name: 'HbA1c', loinc: '59261-8', unit: 'mmol/mol', decimals: 0,
units: [
{ unit: 'mmol/mol', aliases: ['mmol/mol', 'mmol/mol hb', 'mmol per mol'] },
{ unit: '%', aliases: ['%', 'percent', '% hb', '%hb', '% of total hb', 'dcct %'], toCanonical: (value) => (value - 2.15) * 10.929 },
],
synonyms: ['hba1c', 'hb a1c', 'a1c', 'haemoglobin a1c', 'hemoglobin a1c', 'glycated haemoglobin', 'glycated hemoglobin',
'glycosylated haemoglobin', 'glycosylated hemoglobin', 'glycohaemoglobin', 'glycohemoglobin', 'haemoglobin a1c ifcc',
'hba1c ifcc', 'hba1c dcct', 'hba1c ngsp'],
},
{
code: 'glucose_fasting', name: 'Fasting glucose', loinc: '14771-0', unit: 'mmol/L', decimals: 1,
units: [MMOL_PER_L, { unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: dividedBy(18.016) }],
synonyms: ['fasting glucose', 'glucose fasting', 'fasting blood glucose', 'fasting plasma glucose', 'fasting serum glucose',
'fasting blood sugar', 'blood sugar fasting', 'blood glucose fasting', 'plasma glucose fasting', 'fbs', 'fbg', 'fpg'],
},
{
code: 'inr', name: 'INR', loinc: '6301-6', unit: null, decimals: 1, unitOptional: true,
units: [{ unit: '', aliases: ['ratio', 'inr', 'index'] }],
synonyms: ['inr', 'international normalised ratio', 'international normalized ratio', 'pt inr', 'inr ratio',
'prothrombin time inr', 'prothrombin inr'],
},
{
code: 'potassium', name: 'Potassium', loinc: '2823-3', unit: 'mmol/L', decimals: 1, unitOptional: true,
units: [MMOL_PER_L, MEQ_PER_L],
synonyms: ['potassium', 'k', 'k+', 'serum potassium', 'plasma potassium', 'potassium k', 'potassium level'],
},
{
code: 'sodium', name: 'Sodium', loinc: '2951-2', unit: 'mmol/L', decimals: 0, unitOptional: true,
units: [MMOL_PER_L, MEQ_PER_L],
synonyms: ['sodium', 'na', 'na+', 'serum sodium', 'plasma sodium', 'sodium na', 'sodium level'],
},
{
code: 'alt', name: 'ALT', loinc: '1742-6', unit: 'U/L', decimals: 0, unitOptional: true,
units: [UNITS_PER_L],
synonyms: ['alt', 'alat', 'sgpt', 'alt sgpt', 'sgpt alt', 'alanine aminotransferase', 'alanine transaminase',
'alanine amino transferase', 'alanine transferase'],
},
{
code: 'ast', name: 'AST', loinc: '1920-8', unit: 'U/L', decimals: 0, unitOptional: true,
units: [UNITS_PER_L],
synonyms: ['ast', 'asat', 'sgot', 'ast sgot', 'sgot ast', 'aspartate aminotransferase', 'aspartate transaminase',
'aspartate amino transferase', 'aspartate transferase'],
},
{
code: 'vitamin_b12', name: 'Vitamin B12', loinc: '2132-9', unit: 'ng/L', decimals: 0,
units: [
{ unit: 'ng/L', aliases: ['ng/l'] },
{ unit: 'pg/mL', aliases: ['pg/ml'], toCanonical: SAME },
{ unit: 'pmol/L', aliases: ['pmol/l'], toCanonical: times(1.3554) },
],
synonyms: ['vitamin b12', 'vit b12', 'b12', 'vitamin b 12', 'cobalamin', 'serum b12', 'serum vitamin b12', 'cobalamin b12'],
},
{
code: 'cholesterol_total', name: 'Total cholesterol', loinc: '14647-2', unit: 'mmol/L', decimals: 2,
units: [MMOL_PER_L, { unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: dividedBy(38.67) }],
synonyms: ['total cholesterol', 'cholesterol', 'cholesterol total', 'serum cholesterol', 'tc', 't chol', 'total chol'],
},
{
code: 'ldl', name: 'LDL cholesterol', loinc: '22748-8', unit: 'mmol/L', decimals: 2,
units: [MMOL_PER_L, { unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: dividedBy(38.67) }],
synonyms: ['ldl', 'ldl c', 'ldl cholesterol', 'ldl chol', 'low density lipoprotein', 'low density lipoprotein cholesterol',
'ldl calculated', 'calculated ldl', 'ldl cholesterol calculated', 'ldl direct', 'direct ldl'],
},
{
code: 'hdl', name: 'HDL cholesterol', loinc: '14646-4', unit: 'mmol/L', decimals: 2,
units: [MMOL_PER_L, { unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: dividedBy(38.67) }],
synonyms: ['hdl', 'hdl c', 'hdl cholesterol', 'hdl chol', 'high density lipoprotein', 'high density lipoprotein cholesterol'],
},
{
code: 'triglycerides', name: 'Triglycerides', loinc: '14927-8', unit: 'mmol/L', decimals: 2,
units: [MMOL_PER_L, { unit: 'mg/dL', aliases: MG_PER_DL, toCanonical: dividedBy(88.57) }],
synonyms: ['triglycerides', 'triglyceride', 'trigs', 'tg', 'tgs', 'serum triglycerides', 'triacylglycerol', 'triacylglycerols'],
},
{
code: 'tsh', name: 'TSH', loinc: '3016-3', unit: 'mU/L', decimals: 2, unitOptional: true,
units: [{ unit: 'mU/L', aliases: ['mu/l', 'miu/l', 'uiu/ml', 'uu/ml', 'microiu/ml', 'miu per l', 'mu per l'] }],
synonyms: ['tsh', 'thyroid stimulating hormone', 'thyrotropin', 'thyrotrophin', 'tsh 3rd generation', 'serum tsh'],
},
{
code: 'haemoglobin', name: 'Haemoglobin', loinc: '718-7', unit: 'g/L', decimals: 0,
units: [{ unit: 'g/L', aliases: ['g/l'] }, { unit: 'g/dL', aliases: ['g/dl', 'g%', 'gm/dl', 'gm%'], toCanonical: times(10) }],
synonyms: ['haemoglobin', 'hemoglobin', 'hb', 'hgb', 'haemoglobin hb', 'hemoglobin hb', 'haemoglobin level', 'hemoglobin level'],
},
{
// Counts per µL (per mm³) are a thousand times the count in 10⁹/L, so a count without a unit is not charted.
code: 'platelets', name: 'Platelets', loinc: '777-3', unit: '10⁹/L', decimals: 0,
units: [
{ unit: '10⁹/L', aliases: ['10^9/l', 'x10^9/l', '10*9/l', 'x10*9/l', '10e9/l', 'x10e9/l', 'giga/l', '10^3/ul',
'x10^3/ul', '10*3/ul', '10^3/mm3', 'x10^3/mm3', 'k/ul', 'thou/ul', '/nl', '10³/µl', '×10³/µl', '×10⁹/l'] },
{ unit: '/µL', aliases: ['/ul', 'per ul', 'cells/ul', '/mm3', '/mm³', 'per mm3', 'cells/mm3'], toCanonical: dividedBy(1000) },
],
synonyms: ['platelets', 'platelet', 'platelet count', 'platelets count', 'plt', 'plt count', 'thrombocytes', 'thrombocyte count'],
},
{
code: 'urine_acr', name: 'Urine albumin-creatinine ratio', loinc: '14958-3', unit: 'mg/mmol', decimals: 1,
units: [
{ unit: 'mg/mmol', aliases: ['mg/mmol', 'mg/mmol creat', 'mg/mmol creatinine'] },
{ unit: 'mg/g', aliases: ['mg/g', 'mg/g creat', 'mg/g creatinine', 'ug/mg', 'mcg/mg'], toCanonical: dividedBy(8.84) },
],
synonyms: ['urine albumin creatinine ratio', 'albumin creatinine ratio', 'urine acr', 'acr', 'uacr', 'u acr',
'urinary albumin creatinine ratio', 'urine albumin to creatinine ratio', 'albumin to creatinine ratio',
'urinary albumin to creatinine ratio', 'microalbumin creatinine ratio', 'urine microalbumin creatinine ratio',
'albumin creatinine ratio urine'],
},
{
code: 'bp_systolic', name: 'Systolic blood pressure', loinc: '8480-6', unit: 'mmHg', decimals: 0, unitOptional: true,
units: [{ unit: 'mmHg', aliases: ['mmhg', 'mm hg', 'mm[hg]'] }],
synonyms: ['systolic', 'systolic blood pressure', 'systolic bp', 'sbp', 'bp systolic', 'blood pressure systolic'],
},
{
code: 'bp_diastolic', name: 'Diastolic blood pressure', loinc: '8462-4', unit: 'mmHg', decimals: 0, unitOptional: true,
units: [{ unit: 'mmHg', aliases: ['mmhg', 'mm hg', 'mm[hg]'] }],
synonyms: ['diastolic', 'diastolic blood pressure', 'diastolic bp', 'dbp', 'bp diastolic', 'blood pressure diastolic'],
},
];
const BY_CODE = new Map(LAB_TESTS.map((test) => [test.code, test]));
const COMBINED_BLOOD_PRESSURE = new Set(['blood pressure', 'bp', 'b p', 'blood pressure bp']);
// Words that say where the sample came from or pad the name, but never which
// test it is. "Urine" is deliberately absent: urine creatinine is a different test.
const SPECIMEN_PREFIXES = new Set(['serum', 'plasma', 'blood', 's', 'sr', 'p', 'whole']);
const PADDING_SUFFIXES = new Set(['level', 'levels', 'result', 'test', 'concentration', 'value', 'serum', 'plasma', 'blood', 'sr']);
// A name carrying one of these measures something else, even when a bracket
// holds a catalogued name: "Creatinine (urine)", "Vitamin B12 (active)".
const OTHER_MEASURE = new Set(['urine', 'urinary', 'csf', 'fluid', 'stool', 'faecal', 'fecal', 'sweat', 'saliva', 'dialysate',
'ascitic', 'pleural', 'clearance', 'excretion', 'ratio', 'index', 'active', 'holotranscobalamin', 'non']);
/** Lower case, no accents, punctuation as spaces, "+" kept for "K+" and "Na+". */
export function nameKey(value: string): string {
return value
.normalize('NFKD')
.replace(/\p{M}/gu, '')
.toLowerCase()
.replace(/&/g, ' and ')
.replace(/[^a-z0-9+]+/g, ' ')
.trim()
.replace(/\s+/g, ' ');
}
function trimPadding(key: string): string {
const words = key.split(' ');
while (words.length > 1 && SPECIMEN_PREFIXES.has(words[0])) words.shift();
while (words.length > 1 && PADDING_SUFFIXES.has(words[words.length - 1])) words.pop();
return words.join(' ');
}
const SYNONYM_INDEX: Map<string, string> = (() => {
const index = new Map<string, string>();
for (const test of LAB_TESTS) {
for (const synonym of [test.name, test.code.replace(/_/g, ' '), ...test.synonyms]) {
const key = nameKey(synonym);
const existing = index.get(key);
if (existing && existing !== test.code) throw new Error(`Lab catalogue synonym "${synonym}" names two tests`);
index.set(key, test.code);
}
}
return index;
})();
function codeForKey(key: string): string | null {
if (!key) return null;
return SYNONYM_INDEX.get(key) ?? SYNONYM_INDEX.get(trimPadding(key)) ?? null;
}
/**
* The catalogue code for a test name as printed, or null when the name is not
* one the catalogue knows. Matching is on the whole name, never a fragment, so
* "Urine creatinine", "Non-HDL cholesterol" and "Glucose" (not fasting) stay
* unknown. A bracketed part is tried both ways: "ALT (SGPT)", "eGFR (CKD-EPI)".
*/
export function labCodeForName(printedName: string | null | undefined): string | null {
if (!printedName) return null;
const whole = nameKey(printedName);
const direct = codeForKey(whole);
if (direct) return direct;
const outside = codeForKey(nameKey(printedName.replace(/\([^)]*\)|\[[^\]]*\]/g, ' ')));
const inside = [...printedName.matchAll(/\(([^)]*)\)|\[([^\]]*)\]/g)]
.map((match) => codeForKey(nameKey(match[1] ?? match[2] ?? '')))
.filter((code): code is string => Boolean(code));
const candidates = new Set([...(outside ? [outside] : []), ...inside]);
// Two different answers means the name is not one test we can name.
if (candidates.size !== 1) return null;
const [code] = candidates;
if (code !== 'urine_acr' && whole.split(' ').some((word) => OTHER_MEASURE.has(word))) return null;
return code;
}
/** "Blood pressure" printed as one line, e.g. "138/86 mmHg". */
export function isCombinedBloodPressure(printedName: string | null | undefined): boolean {
if (!printedName) return false;
const key = nameKey(printedName.replace(/\([^)]*\)/g, ' '));
return COMBINED_BLOOD_PRESSURE.has(key) || COMBINED_BLOOD_PRESSURE.has(trimPadding(key));
}
export function findLabTest(code: string | null | undefined): LabTest | null {
return code ? BY_CODE.get(code) ?? null : null;
}
export function isLabCode(code: string): boolean {
return BY_CODE.has(code);
}
const SUPERSCRIPTS: Record<string, string> = { '⁰': '0', '¹': '1', '²': '2', '³': '3', '⁴': '4', '⁵': '5', '⁶': '6', '⁷': '7', '⁸': '8', '⁹': '9' };
/** A comparable form of a printed unit: "×10⁹/L", "x10^9/l" and "10*9/L" all become "10^9/l". */
export function unitKey(value: string): string {
return value
.replace(/[⁰¹²³⁴⁵⁶⁷⁸⁹]+/g, (digits) => `^${[...digits].map((digit) => SUPERSCRIPTS[digit]).join('')}`)
.normalize('NFKC')
.toLowerCase()
.replace(/[µμ]/g, 'u')
.replace(/\s+/g, '')
.replace(/×/g, '')
.replace(/\*/g, '^')
.replace(/^x(?=10)/, '')
.replace(/^10e(?=\d)/, '10^')
.replace(/([a-z\]])\^(?=\d)/g, '$1');
}
/** The accepted unit a printed unit means for this test, or null if unknown. */
export function findLabUnit(test: LabTest, printedUnit: string | null | undefined): LabUnit | null {
const key = unitKey(printedUnit ?? '');
if (!key) return test.unitOptional ? test.units[0] : null;
return test.units.find((unit) => unitKey(unit.unit) === key || unit.aliases?.some((alias) => unitKey(alias) === key)) ?? null;
}
function round(value: number, decimals: number): number {
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
}
/**
* The value in the test's canonical unit, for charts and the check engine.
* Null when the test is not catalogued, the value is not a number, or the
* unit is not one the catalogue can convert. The printed value is untouched.
*/
export function normalizeLabValue(code: string | null | undefined, value: number | null | undefined, printedUnit: string | null | undefined):
{ value: number; unit: string | null } | null {
const test = findLabTest(code);
if (!test || value === null || value === undefined || !Number.isFinite(value)) return null;
const unit = findLabUnit(test, printedUnit);
if (!unit) return null;
const converted = unit.toCanonical ? round(unit.toCanonical(value), test.decimals) : value;
return Number.isFinite(converted) ? { value: converted, unit: test.unit } : null;
}
/** What the app's typing screen and the API reference show for each test. */
export function labCatalogue() {
return LAB_TESTS.map((test) => ({
code: test.code,
name: test.name,
loinc: test.loinc,
unit: test.unit,
units: test.units.map((unit) => unit.unit).filter(Boolean),
}));
}
const KNOWN_UNIT_KEYS = new Set(LAB_TESTS.flatMap((test) => test.units.flatMap((unit) => [unit.unit, ...(unit.aliases ?? [])]))
.map(unitKey).filter(Boolean));
/** Whether text is a unit any catalogued test is printed in, e.g. "mmol/L". */
export function isKnownLabUnit(text: string): boolean {
return KNOWN_UNIT_KEYS.has(unitKey(text));
}