import { findLabTest, findLabUnit } from '../labs/catalog.js';
import type { CheckInput, CheckLab } from './engine.js';
/**
* CHK-10 (egfrFromCreatinine): race-free CKD-EPI 2021 (NIDDK, National Kidney Foundation), adults only:
* 142 x min(Scr/k, 1)^a x max(Scr/k, 1)^-1.200 x 0.9938^age x 1.012 [female]; Scr mg/dL, k 0.7 F / 0.9 M, a -0.241 F / -0.302 M.
*/
export const CKD_EPI_2021 = 'CKD-EPI 2021 (race-free)';
/** µmol/L per mg/dL, as the CKD-EPI publications use. */
export const CREATININE_UMOL_PER_MG_DL = 88.4;
export type Sex = 'female' | 'male';
/** eGFR in mL/min/1.73m², unrounded. */
export function ckdEpi2021(creatinineMgDl: number, age: number, sex: Sex): number {
const k = sex === 'female' ? 0.7 : 0.9;
const a = sex === 'female' ? -0.241 : -0.302;
const ratio = creatinineMgDl / k;
return 142 * Math.min(ratio, 1) ** a * Math.max(ratio, 1) ** -1.2 * 0.9938 ** age * (sex === 'female' ? 1.012 : 1);
}
/** A sex the equation can use, or null. Anything else is not guessed. */
export function sexForEquation(sex: string | null | undefined): Sex | null {
const said = (sex ?? '').trim().toLowerCase();
if (['female', 'f', 'woman', 'girl'].includes(said)) return 'female';
if (['male', 'm', 'man', 'boy'].includes(said)) return 'male';
return null;
}
/** Creatinine in mg/dL from a value and its printed unit; null when the unit is missing or not one we know. */
export function creatinineMgDl(value: number, unit: string | null | undefined): number | null {
const test = findLabTest('creatinine');
const found = test && unit ? findLabUnit(test, unit) : null;
if (!found || !Number.isFinite(value) || value <= 0) return null;
if (found.unit === 'mg/dL') return value;
if (found.unit === test!.unit) return value / CREATININE_UMOL_PER_MG_DL;
if (found.unit === 'mmol/L') return (value * 1000) / CREATININE_UMOL_PER_MG_DL;
return null;
}
export type EgfrNotCalculated = 'age_not_recorded' | 'age_under_18' | 'sex_not_recorded' | 'creatinine_not_exact' | 'creatinine_unit_unknown';
const NOT_CALCULATED_PLAIN: Record<EgfrNotCalculated, string> = {
age_not_recorded: 'Age: needed to calculate eGFR from creatinine (CKD-EPI 2021).',
age_under_18: 'eGFR was not calculated from creatinine: CKD-EPI 2021 is for adults.',
sex_not_recorded: 'Sex: needed to calculate eGFR from creatinine (CKD-EPI 2021).',
creatinine_not_exact: 'eGFR was not calculated: the creatinine result is not an exact number.',
creatinine_unit_unknown: 'eGFR was not calculated: the creatinine unit is not recorded or not recognised.',
};
export type CalculatedEgfr =
| { status: 'calculated'; value: number; value_unrounded: number; unit: string; label: 'calculated'; formula: string;
observed_at: string; inputs: { creatinine: { value: number; unit: string; value_mg_dl: number; observed_at: string }; age: number; sex: Sex } }
| { status: 'not_calculated'; reason: EgfrNotCalculated; plain: string; formula: string };
/**
* With a creatinine result and no eGFR at all, the eGFR to use and how it was worked out. The latest creatinine is
* used, dated like the creatinine. When only the birth year is known, the older age is used (the lower eGFR).
* Null when there is no creatinine, or there is an eGFR.
*/
export function calculateEgfr(input: CheckInput): { lab: CheckLab | null; output: CalculatedEgfr } | null {
if (input.labs.some((lab) => lab.code === 'egfr')) return null;
const creatinine = [...input.labs].filter((lab) => lab.code === 'creatinine').sort((x, y) => x.observedAt.localeCompare(y.observedAt)).pop();
if (!creatinine) return null;
const notCalculated = (reason: EgfrNotCalculated) => ({ lab: null, output: { status: 'not_calculated' as const, reason, plain: NOT_CALCULATED_PLAIN[reason], formula: CKD_EPI_2021 } });
const age = input.subject.ageRange?.max ?? input.subject.age;
if (age === null || age === undefined) return notCalculated('age_not_recorded');
if ((input.subject.ageRange?.min ?? age) < 18) return notCalculated('age_under_18');
const sex = sexForEquation(input.subject.sex);
if (!sex) return notCalculated('sex_not_recorded');
if (creatinine.comparator) return notCalculated('creatinine_not_exact');
const unit = creatinine.unit ?? creatinine.evidence?.result.unit ?? null;
const mgDl = creatinineMgDl(creatinine.value, unit);
if (mgDl === null) return notCalculated('creatinine_unit_unknown');
const exact = ckdEpi2021(mgDl, age, sex);
// Rounded to a whole number, as laboratories report eGFR.
const value = Math.round(exact);
return {
lab: { code: 'egfr', value, observedAt: creatinine.observedAt, comparator: null },
output: { status: 'calculated', value, value_unrounded: Math.round(exact * 10) / 10, unit: 'mL/min/1.73m²', label: 'calculated', formula: CKD_EPI_2021,
observed_at: creatinine.observedAt,
inputs: { creatinine: { value: creatinine.value, unit: unit!, value_mg_dl: Math.round(mgDl * 100) / 100, observed_at: creatinine.observedAt }, age, sex } },
};
}