// ============================================================ // Pure pediatric calculator helpers. // // Keep clinical math out of React components and DOM event handlers. // High-risk tables such as Rosner BP, Fenton LMS, and bilirubin // thresholds should be added here only after legacy vectors exist. // ============================================================ export type WeightFormulaBand = | 'APLS 0-12 mo' | 'APLS 1-5 yr' | 'APLS 6-12 yr' | 'Best Guess 5-14 yr'; export interface EstimatedWeight { weight: number; formulaLabel: WeightFormulaBand; all: { apls: number; bestGuess: number; }; } export function parseAgeMonths(input: string | number | null | undefined): number | null { if (input == null) return null; const value = String(input).toLowerCase().trim(); if (!value) return null; if (/^[\d.]+$/.test(value)) { const parsed = Number.parseFloat(value); return Number.isNaN(parsed) ? null : parsed; } let total = 0; let matched = false; const years = value.match(/([\d.]+)\s*(?:years|year|yrs|yr|y)(?![a-z])/); if (years) { total += Number.parseFloat(years[1]) * 12; matched = true; } const months = value.match(/([\d.]+)\s*(?:months|month|mos|mo|m)(?![a-z])/); if (months) { total += Number.parseFloat(months[1]); matched = true; } const weeks = value.match(/([\d.]+)\s*(?:weeks|week|wks|wk|w)(?![a-z])/); if (weeks) { total += Number.parseFloat(weeks[1]) * 7 / 30.4375; matched = true; } const days = value.match(/([\d.]+)\s*(?:days|day|d)(?![a-z])/); if (days) { total += Number.parseFloat(days[1]) / 30.4375; matched = true; } return matched ? total : null; } export function formatAgeMonths(months: number): string { if (months < 1) { const days = Math.round(months * 30.4375); return `${days} day${days === 1 ? '' : 's'} (${months.toFixed(2)} mo)`; } if (months < 24) return `${roundTo(months, 1)} months`; let years = Math.floor(months / 12); let remainingMonths = Math.round(months - years * 12); if (remainingMonths === 12) { years += 1; remainingMonths = 0; } return `${years} yr${remainingMonths ? ` ${remainingMonths} mo` : ''} (${Math.round(months)} mo total)`; } export interface WeightBasedDoseInput { weightKg: number; dosePerKg: number; frequencyPerDay?: number; maxSingleDoseMg?: number | null; concentrationMgPerMl?: number | null; } export interface WeightBasedDoseResult { singleDoseMg: number; dailyDoseMg: number; frequencyPerDay: number; capped: boolean; volumeMl: number | null; } export type GcsSeverity = 'Mild' | 'Moderate' | 'Severe (Coma)'; export interface GcsResult { total: number; severity: GcsSeverity; } export function roundTo(value: number, places: number): number { const multiplier = Math.pow(10, places); return Math.round(value * multiplier) / multiplier; } // APLS (Luscombe & Owens 2007 / APLS-UK 2016) and Best Guess // (Tinning & Acworth 2007). Ported from public/js/calc-math.js. export function estimateWeightFromAgeMonths(months: number | null | undefined): EstimatedWeight | null { if (months == null || Number.isNaN(months) || months < 0) return null; const years = months / 12; let apls: number; if (months < 12) apls = 0.5 * months + 4; else if (years <= 5) apls = 2 * years + 8; else apls = 3 * years + 7; let bestGuess: number; if (months < 12) bestGuess = (months + 9) / 2; else if (years <= 5) bestGuess = 2 * (years + 5); else bestGuess = 4 * years; let formulaLabel: WeightFormulaBand; if (years < 13) { if (months < 12) formulaLabel = 'APLS 0-12 mo'; else if (years <= 5) formulaLabel = 'APLS 1-5 yr'; else formulaLabel = 'APLS 6-12 yr'; } else { formulaLabel = 'Best Guess 5-14 yr'; } const primary = years < 13 ? apls : bestGuess; return { weight: Math.max(0.3, roundTo(primary, 1)), formulaLabel, all: { apls: roundTo(apls, 1), bestGuess: roundTo(bestGuess, 1), }, }; } // Mosteller formula used by the legacy BSA calculator: // BSA (m2) = sqrt(height(cm) * weight(kg) / 3600). export function calculateMostellerBsa(weightKg: number, heightCm: number): number | null { if (!Number.isFinite(weightKg) || !Number.isFinite(heightCm) || weightKg <= 0 || heightCm <= 0) { return null; } return Math.sqrt((heightCm * weightKg) / 3600); } // Ported from public/js/calculators.js weight-based dosing panel. export function calculateWeightBasedDose(input: WeightBasedDoseInput): WeightBasedDoseResult | null { const frequencyPerDay = input.frequencyPerDay ?? 1; const maxSingleDoseMg = input.maxSingleDoseMg ?? 0; const concentrationMgPerMl = input.concentrationMgPerMl ?? 0; if ( !Number.isFinite(input.weightKg) || !Number.isFinite(input.dosePerKg) || !Number.isFinite(frequencyPerDay) || input.weightKg <= 0 || input.dosePerKg <= 0 || frequencyPerDay <= 0 ) { return null; } let singleDoseMg = input.weightKg * input.dosePerKg; let capped = false; if (maxSingleDoseMg > 0 && singleDoseMg > maxSingleDoseMg) { singleDoseMg = maxSingleDoseMg; capped = true; } return { singleDoseMg, dailyDoseMg: singleDoseMg * frequencyPerDay, frequencyPerDay, capped, volumeMl: concentrationMgPerMl > 0 ? singleDoseMg / concentrationMgPerMl : null, }; } // Dose formatter used by Bedside drug tables. Mirrors the vanilla // S.dStr helper in public/js/bedside/shared.js — same rounding, same // cap logic, same label format. Pure string builder, no HTML. // Example: formatDose(30, 0.1, 4) → "3 mg (0.1 mg/kg, max 4 mg)" // formatDose(60, 0.1, 4) → "4 mg (0.1 mg/kg, max 4 mg · capped)" export interface FormattedDose { value: number; unit: string; capped: boolean; perKg: number; max: number | null; label: string; } export function formatDose(weightKg: number, perKg: number, max?: number | null, unit = 'mg'): FormattedDose { const raw = weightKg * perKg; const rounded = Math.round(raw * 100) / 100; const hasMax = max != null && max > 0; const capped = hasMax && rounded > max!; const value = capped ? max! : rounded; let perKgStr = `(${perKg} ${unit}/kg`; if (hasMax) perKgStr += `, max ${max} ${unit}`; if (capped) perKgStr += ' · capped'; perKgStr += ')'; return { value, unit, capped, perKg, max: hasMax ? max! : null, label: `${value} ${unit} ${perKgStr}`, }; } // Child/adult and infant GCS scoring share the same numeric interpretation. // Ported from public/js/calculators.js. export function calculateGcs(eye: number, verbal: number, motor: number): GcsResult | null { if ( !Number.isFinite(eye) || !Number.isFinite(verbal) || !Number.isFinite(motor) || eye < 1 || eye > 4 || verbal < 1 || verbal > 5 || motor < 1 || motor > 6 ) { return null; } const total = eye + verbal + motor; let severity: GcsSeverity; if (total <= 8) severity = 'Severe (Coma)'; else if (total <= 12) severity = 'Moderate'; else severity = 'Mild'; return { total, severity }; }