Closes the Calculators migration. AAP 2017 BP percentile (Rosner
quantile splines) was the biggest table-driven calculator in the
codebase: 6 height-LMS arrays × 218 entries + 4 spline coefficient
matrices × 99 rows × 13 terms = ~3,500 numeric constants. Every one
ported verbatim. 14 parity tests prove the TS port returns identical
percentile and classification outputs to the vanilla calculators.js.
shared/clinical/bp.ts — 571 lines
Generated from public/js/calculators.js via awk-extracted lines
89-94 (LMS) and 97-503 (coefficients), wrapped in TS export
declarations. No rewriting, no reformatting, no reordering — the
data bytes are identical to the vanilla source.
Math (calcHeightPercentile, computeBpPercentile,
classifyBpFromPercentiles) ported verbatim from calculators.js:
505-608. Exports a top-level computeBp() helper returning
{ sysPercentile, diaPercentile, heightPercentile, sysClass,
diaClass, classification }.
scripts/capture-calc-vectors.js — BP cases added
Uses new Function() to evaluate the raw LMS + coefficient blocks
from calculators.js directly, then runs the vanilla math against
14 carefully chosen test cases:
• Typical pediatric ages (3, 5, 8, 10, 12 years, both sexes)
• Adult-threshold cross-over (age 13 — uses absolute mmHg cutoffs)
• Stage 1 / Stage 2 hypertension boundaries
• Edge-of-domain (age 1, age 17)
• Tall / short height-percentile outliers
Fixture regenerated to 14 Bhutani + 58 AAP + 13 Fenton + 8 neonatal
+ 12 BMI + 14 BP = 117 total vectors.
shared/clinical/bp.test.ts — exact-match parity
sysPercentile / diaPercentile are integer selections from 99
candidate predicted values, so tests use .toBe() for exact match.
heightPercentile uses toBeCloseTo(6) (double-precision float).
Classification strings must match exactly. 14/14 pass.
client/src/pages/CalculatorPanels.tsx — BpPanel added
Age/sex/height/SBP/DBP inputs, validation (age 1-17, height 50-200
cm), color-coded overall classification + per-measurement
percentile and tier (Normal / Elevated / Stage 1 / Stage 2) in a
4-column result grid with the height percentile for context.
client/src/pages/Calculators.tsx
Dispatch wires bp → BpPanel. PILLS['bp'].ported = true.
LegacyPanel is now dead code — every pill has a real implementation.
Final test suite: 5 files, 136 tests green
(19 calculators + 70 bilirubin + 21 fenton/neonatal + 12 BMI + 14 BP)
39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
// ============================================================
|
|
// BP PERCENTILE parity tests. Vectors captured directly from the
|
|
// vanilla calculators.js height-LMS + Rosner spline coefficients.
|
|
// ============================================================
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { computeBp, type Sex, type BpClassification } from './bp';
|
|
|
|
interface BpCase {
|
|
name: string;
|
|
inputs: { ageYears: number; sex: Sex; heightCm: number; sbp: number; dbp: number };
|
|
output: {
|
|
sysPercentile: number;
|
|
diaPercentile: number;
|
|
heightPercentile: number;
|
|
sysClass: BpClassification;
|
|
diaClass: BpClassification;
|
|
classification: BpClassification;
|
|
};
|
|
}
|
|
interface Vectors { bp: BpCase[] }
|
|
|
|
const vectors: Vectors = JSON.parse(
|
|
fs.readFileSync(path.join(__dirname, '..', '..', 'e2e', 'fixtures', 'calc-vectors.json'), 'utf8'),
|
|
);
|
|
|
|
describe('AAP 2017 BP percentile — parity with vanilla calculators.js', () => {
|
|
it.each(vectors.bp)('$name', ({ inputs, output }) => {
|
|
const actual = computeBp(inputs.ageYears, inputs.sex, inputs.heightCm, inputs.sbp, inputs.dbp);
|
|
expect(actual.sysPercentile).toBe(output.sysPercentile);
|
|
expect(actual.diaPercentile).toBe(output.diaPercentile);
|
|
expect(actual.heightPercentile).toBeCloseTo(output.heightPercentile, 6);
|
|
expect(actual.sysClass).toBe(output.sysClass);
|
|
expect(actual.diaClass).toBe(output.diaClass);
|
|
expect(actual.classification).toBe(output.classification);
|
|
});
|
|
});
|