Nine of the ten Calculator pills now run in React. Only BP Percentile
(Rosner quantile splines, ~3,000 hand-transcribed coefficients across
6 LMS arrays and 4 spline matrices) remains legacy-linked; that port
deserves its own dedicated session with extra care.
shared/clinical/bmi.ts — CDC 2000 BMI-for-age
• bmiLMS table ported byte-for-byte from calculators.js:739
(74 LMS triples = 37 age points × 2 sexes, 24-240 months).
• normalCDF (Abramowitz & Stegun), calcBmiPercentile, classifyBMI
(including the %-of-95th severe-obesity split), and a top-level
computeBmi helper — all verbatim translations of the vanilla math.
• classification labels preserved exactly so existing e2e screenshots
or reporting continue to read the same text ('Class 2 Severe
Obesity', 'Healthy Weight', etc.).
shared/clinical/bmi.test.ts — 12 captured vectors covering:
both sexes, edges (2y + 20y), interpolated-between-keys (13m),
each classification cliff (underweight / healthy / overweight /
obese / severe class 2 / severe class 3), and the age-clamping
branches (<24 mo and >240 mo). All 12 pass at 10-decimal precision
(percentile to 6 places since the vanilla rounds to 2).
scripts/capture-calc-vectors.js — BMI section added
Same pattern as bilirubin / Fenton: the vanilla data + math are
inlined verbatim, the script runs 12 chosen cases, and writes to
e2e/fixtures/calc-vectors.json. Re-run after any upstream change.
client/src/pages/CalculatorPanels.tsx (new)
• BmiPanel — age/sex/weight/height inputs, calls computeBmi,
renders color-coded classification badge with BMI, percentile,
Z, and % of 95th when percentile ≥85.
• VitalsPanel — 8-band age selector (premie → >12 yr).
VITALS_DATA ported verbatim from calculators.js:1703-1831 with
every HR/RR/SBP/DBP/temp/weight/SpO₂ range and clinical notes
preserved entry-for-entry.
• ResusPanel — weight input drives all 14 drugs (Adenosine,
Amiodarone, Atropine, CaCl, Ca-gluconate, Dextrose, Epi,
Hydrocortisone, Insulin, Lidocaine, Mg, Naloxone, NaHCO₃) with
calc() closures ported verbatim from RESUS_MEDS lines 1873-2050.
Category colors + labels preserved.
• EquipmentPanel — 9-band age selector (premie → 16+ yr).
EQUIP_DATA ported verbatim from calculators.js:2173-2228 with
12 equipment sizes per band (BVM, NPA, OPA, blade, ETT, LMA,
Glidescope, IV, CVL, NGT, chest tube, Foley).
client/src/pages/Calculators.tsx
Dispatch wires bmi → BmiPanel, vitals → VitalsPanel, resus →
ResusPanel, equipment → EquipmentPanel. PILLS flags all four as
ported: true. Only bp remains on LegacyPanel.
Backend tsc + client tsc + vite build + 122/122 vitest (19 calc +
70 bili + 21 fenton/neonatal + 12 bmi) all green.
39 lines
1.6 KiB
TypeScript
39 lines
1.6 KiB
TypeScript
// ============================================================
|
|
// BMI-FOR-AGE parity tests.
|
|
// ============================================================
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { computeBmi } from './bmi';
|
|
import type { Sex } from './fenton';
|
|
|
|
interface BmiCase {
|
|
name: string;
|
|
inputs: { weightKg: number; heightCm: number; ageMonths: number; sex: Sex };
|
|
output: {
|
|
bmi: number; z: number; percentile: number;
|
|
L: number; M: number; S: number;
|
|
classification: { label: string; pctOf95: number; bmi95: number };
|
|
};
|
|
}
|
|
interface Vectors { bmi: BmiCase[] }
|
|
|
|
const vectors: Vectors = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, '..', '..', 'e2e', 'fixtures', 'calc-vectors.json'), 'utf8'),
|
|
);
|
|
|
|
describe('CDC 2000 BMI-for-age — parity with vanilla calculators.js', () => {
|
|
it.each(vectors.bmi)('$name', ({ inputs, output }) => {
|
|
const actual = computeBmi(inputs.weightKg, inputs.heightCm, inputs.ageMonths, inputs.sex);
|
|
expect(actual.bmi).toBeCloseTo(output.bmi, 10);
|
|
expect(actual.z).toBeCloseTo(output.z, 10);
|
|
expect(actual.percentile).toBeCloseTo(output.percentile, 6);
|
|
expect(actual.L).toBeCloseTo(output.L, 10);
|
|
expect(actual.M).toBeCloseTo(output.M, 10);
|
|
expect(actual.S).toBeCloseTo(output.S, 10);
|
|
expect(actual.classification.label).toBe(output.classification.label);
|
|
expect(actual.classification.pctOf95).toBeCloseTo(output.classification.pctOf95, 6);
|
|
expect(actual.classification.bmi95).toBeCloseTo(output.classification.bmi95, 10);
|
|
});
|
|
});
|