// ============================================================ // FENTON 2013 — preterm growth LMS for weight-for-GA. Table // ported VERBATIM from public/js/calculators.js:1168-1183 // (15 GA weeks × 2 sexes × {L,M,S}). Parity against the vanilla // implementation pinned by e2e/fixtures/calc-vectors.json + // fenton.test.ts. // // Reference: // Fenton TR, Kim JH. A systematic review and meta-analysis // to revise the Fenton growth chart for preterm infants. // BMC Pediatr 2013;13:59. // ============================================================ export type Sex = 'male' | 'female'; export interface Lms { L: number; M: number; S: number } export interface FentonResult { L: number; M: number; S: number; z: number; percentile: number; } // Verbatim from calculators.js:1168-1183. export const fentonLMS: Record> = { male: { 22: { L: 0.21, M: 496, S: 0.17 }, 24: { L: 0.21, M: 660, S: 0.17 }, 26: { L: 0.21, M: 870, S: 0.16 }, 28: { L: 0.20, M: 1124, S: 0.15 }, 30: { L: 0.18, M: 1430, S: 0.14 }, 32: { L: 0.15, M: 1795, S: 0.14 }, 34: { L: 0.12, M: 2230, S: 0.13 }, 36: { L: 0.08, M: 2710, S: 0.13 }, 38: { L: 0.04, M: 3195, S: 0.12 }, 40: { L: 0.01, M: 3530, S: 0.12 }, 42: { L: -0.02, M: 3820, S: 0.12 }, 44: { L: -0.04, M: 4200, S: 0.12 }, 46: { L: -0.06, M: 4680, S: 0.12 }, 48: { L: -0.07, M: 5200, S: 0.12 }, 50: { L: -0.08, M: 5760, S: 0.12 }, }, female: { 22: { L: 0.23, M: 474, S: 0.17 }, 24: { L: 0.22, M: 610, S: 0.17 }, 26: { L: 0.22, M: 810, S: 0.16 }, 28: { L: 0.21, M: 1040, S: 0.15 }, 30: { L: 0.19, M: 1330, S: 0.14 }, 32: { L: 0.16, M: 1680, S: 0.14 }, 34: { L: 0.12, M: 2090, S: 0.13 }, 36: { L: 0.08, M: 2540, S: 0.13 }, 38: { L: 0.04, M: 3000, S: 0.12 }, 40: { L: 0.01, M: 3340, S: 0.12 }, 42: { L: -0.02, M: 3630, S: 0.12 }, 44: { L: -0.04, M: 4010, S: 0.12 }, 46: { L: -0.06, M: 4470, S: 0.12 }, 48: { L: -0.07, M: 4970, S: 0.12 }, 50: { L: -0.08, M: 5510, S: 0.12 }, }, }; // Verbatim from calculators.js:2299-2311. export function interpolateLMS(table: Record, val: number): Lms { const keys = Object.keys(table).map(Number).sort((a, b) => a - b); if (val <= keys[0]) return table[keys[0]]; if (val >= keys[keys.length - 1]) return table[keys[keys.length - 1]]; for (let i = 0; i < keys.length - 1; i++) { if (val >= keys[i] && val <= keys[i + 1]) { const t = (val - keys[i]) / (keys[i + 1] - keys[i]); const lms1 = table[keys[i]]; const lms2 = table[keys[i + 1]]; return { L: lms1.L + t * (lms2.L - lms1.L), M: lms1.M + t * (lms2.M - lms1.M), S: lms1.S + t * (lms2.S - lms1.S), }; } } return table[keys[0]]; } // Verbatim from calculators.js:2313-2316. export function calcZ(value: number, L: number, M: number, S: number): number { if (L === 0) return Math.log(value / M) / S; return (Math.pow(value / M, L) - 1) / (L * S); } // Abramowitz & Stegun normal CDF approximation, verbatim from // calculators.js:2318-2326. export function zToPercentile(z: number): number { const t = 1 / (1 + 0.2316419 * Math.abs(z)); const d = 0.3989423 * Math.exp(-z * z / 2); let p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))); if (z > 0) p = 1 - p; return p * 100; } export function fentonWeightForAge(gaWeeks: number, weightGrams: number, sex: Sex): FentonResult { const lms = interpolateLMS(fentonLMS[sex], gaWeeks); const z = calcZ(weightGrams, lms.L, lms.M, lms.S); const percentile = zToPercentile(z); return { L: lms.L, M: lms.M, S: lms.S, z, percentile }; } // Clinical classification — per AAP 2017 / Fenton 2013: // SGA: <10th percentile, LGA: >90th, AGA: between. export type SizeForAge = 'SGA' | 'AGA' | 'LGA'; export function classifySizeForAge(percentile: number): SizeForAge { if (percentile < 10) return 'SGA'; if (percentile > 90) return 'LGA'; return 'AGA'; }