// ============================================================ // calc-math.js — Pure, side-effect-free dose / score math. // Dual-exported: browser (window._PED_MATH) + Node (module.exports). // Keep this file free of DOM references so it can be unit-tested // under Node with `node --test test/`. // ============================================================ (function() { function roundTo(v, places) { var m = Math.pow(10, places); return Math.round(v * m) / m; } // ── Age → Weight ──────────────────────────────────────────── // APLS (Luscombe & Owens 2007 / APLS-UK 2016) and Best Guess // (Tinning & Acworth 2007). For ages ≥13 yr we use Best Guess // as primary; otherwise APLS. function estimateWeightFromAgeMonths(months) { if (months == null || isNaN(months) || months < 0) return null; var years = months / 12; var apls; if (months < 12) apls = 0.5 * months + 4; else if (years <= 5) apls = 2 * years + 8; else apls = 3 * years + 7; var bg; if (months < 12) bg = (months + 9) / 2; else if (years <= 5) bg = 2 * (years + 5); else bg = 4 * years; var primary = years < 13 ? apls : bg; var bandLabel; if (years < 13) { if (months < 12) bandLabel = 'APLS 0-12 mo'; else if (years <= 5) bandLabel = 'APLS 1-5 yr'; else bandLabel = 'APLS 6-12 yr'; } else { bandLabel = 'Best Guess 5-14 yr'; } return { weight: Math.max(0.3, roundTo(primary, 1)), formulaLabel: bandLabel, all: { apls: roundTo(apls, 1), bestGuess: roundTo(bg, 1) } }; } // ── Parkland fluid resuscitation (burn) ───────────────────── // 4 mL × weight(kg) × %TBSA over 24 h; half in first 8 h. function parkland(weightKg, tbsaPct) { if (!weightKg || weightKg <= 0 || !tbsaPct || tbsaPct <= 0) return null; var total = 4 * weightKg * tbsaPct; var first8 = total / 2; var next16 = total - first8; return { totalMl: Math.round(total), first8Ml: Math.round(first8), next16Ml: Math.round(next16), rateFirst8: Math.round(first8 / 8), rateNext16: Math.round(next16 / 16) }; } // ── Maintenance fluids (Holliday-Segar 4-2-1 rule) ───────── function maintenance421(weightKg) { if (!weightKg || weightKg <= 0) return null; if (weightKg <= 10) return Math.round(weightKg * 4); if (weightKg <= 20) return Math.round(40 + (weightKg - 10) * 2); return Math.round(60 + (weightKg - 20)); } // ── PRAM score (asthma, 0-12) ─────────────────────────────── function pramScore(spo2, retractions, scalene, airEntry, wheeze) { var total = [spo2, retractions, scalene, airEntry, wheeze] .reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0); var severity = total <= 3 ? 'Mild' : total <= 7 ? 'Moderate' : 'Severe'; return { total: total, severity: severity }; } // ── Westley croup score (0-17) ────────────────────────────── function westleyScore(conscious, cyanosis, stridor, airEntry, retractions) { var total = [conscious, cyanosis, stridor, airEntry, retractions] .reduce(function(s, v) { return s + (parseInt(v, 10) || 0); }, 0); var severity; if (total <= 2) severity = 'Mild'; else if (total <= 5) severity = 'Moderate'; else if (total <= 11) severity = 'Severe'; else severity = 'Impending respiratory failure'; return { total: total, severity: severity }; } // ── Epinephrine: anaphylaxis IM vs arrest IV ──────────────── // IMPORTANT: different concentrations (1:1,000 vs 1:10,000). function epiAnaphylaxisIM(weightKg) { if (!weightKg || weightKg <= 0) return null; var mg = Math.min(weightKg * 0.01, 0.5); return { doseMg: roundTo(mg, 2), volumeMl_1in1000: roundTo(mg, 2) }; } function epiArrestIV(weightKg) { if (!weightKg || weightKg <= 0) return null; var mg = Math.min(weightKg * 0.01, 1); return { doseMg: roundTo(mg, 2), volumeMl_1in10000: roundTo(mg * 10, 2) }; } // ── NRP epinephrine (neonatal resus) ──────────────────────── // IV/IO 0.01-0.03 mg/kg = 0.1-0.3 mL/kg of 1:10,000. function nrpEpiIV(weightKg) { if (!weightKg || weightKg <= 0) return null; return { lowMg: roundTo(weightKg * 0.01, 2), highMg: roundTo(weightKg * 0.03, 2), lowMl: roundTo(weightKg * 0.1, 2), highMl: roundTo(weightKg * 0.3, 2) }; } // ── RSI drug doses (selected) ─────────────────────────────── function rsiKetamine(weightKg) { if (!weightKg) return null; return { lowMg: Math.min(roundTo(weightKg * 1.5, 1), 150), highMg: Math.min(roundTo(weightKg * 2, 1), 200) }; } function rsiRocuronium(weightKg) { if (!weightKg) return null; return { mg: Math.min(roundTo(weightKg * 1.2, 1), 100) }; } function rsiSuccinylcholine(weightKg) { if (!weightKg) return null; var perKg = weightKg < 10 ? 2 : 1.5; return { mg: Math.min(roundTo(weightKg * perKg, 1), 150), perKg: perKg }; } // ── Minimum acceptable systolic BP (1-10 yr) ──────────────── function minSBP(years) { if (years == null || isNaN(years)) return null; if (years < 1) return 70; if (years > 10) return 90; return 70 + 2 * years; } // ── ETT (endotracheal tube) sizing by age ─────────────────── function ettSize(ageYears) { if (ageYears == null || ageYears < 0) return null; var uncuffed = ageYears / 4 + 4; var cuffed = ageYears / 4 + 3.5; return { uncuffedMm: roundTo(uncuffed, 1), cuffedMm: roundTo(cuffed, 1), depthCm: roundTo(uncuffed * 3, 1) }; } // ── Lund-Browder regional TBSA by age bracket ─────────────── // Used for burns TBSA calculation. Bracket: 'infant', 'young', // 'child', 'adol', 'adult' — per 0-1, 1-5, 5-10, 10-15, >15 yr. var LB_BRACKETS = ['infant', 'young', 'child', 'adol', 'adult']; var LB = { head: [18, 13, 11, 9, 7], neck: [2, 2, 2, 2, 2], ant_trunk: [13, 13, 13, 13, 13], post_trunk: [13, 13, 13, 13, 13], r_buttock: [2.5, 2.5, 2.5, 2.5, 2.5], l_buttock: [2.5, 2.5, 2.5, 2.5, 2.5], genital: [1, 1, 1, 1, 1], r_uparm: [4, 4, 4, 4, 4], l_uparm: [4, 4, 4, 4, 4], r_forearm: [3, 3, 3, 3, 3], l_forearm: [3, 3, 3, 3, 3], r_hand: [2.5, 2.5, 2.5, 2.5, 2.5], l_hand: [2.5, 2.5, 2.5, 2.5, 2.5], r_thigh: [5.5, 8, 8.5, 9, 9.5], l_thigh: [5.5, 8, 8.5, 9, 9.5], r_leg: [5, 5.5, 6, 6.5, 7], l_leg: [5, 5.5, 6, 6.5, 7], r_foot: [3.5, 3.5, 3.5, 3.5, 3.5], l_foot: [3.5, 3.5, 3.5, 3.5, 3.5] }; // involvements: { regionKey: percentOfRegionInvolved (0-100), ... } function lundBrowderTBSA(ageBracket, involvements) { var idx = LB_BRACKETS.indexOf(ageBracket); if (idx < 0) return null; var total = 0; Object.keys(LB).forEach(function(key) { var pct = (involvements && involvements[key]) || 0; if (pct < 0) pct = 0; if (pct > 100) pct = 100; total += LB[key][idx] * (pct / 100); }); return roundTo(total, 1); } var API = { roundTo: roundTo, estimateWeightFromAgeMonths: estimateWeightFromAgeMonths, parkland: parkland, maintenance421: maintenance421, pramScore: pramScore, westleyScore: westleyScore, epiAnaphylaxisIM: epiAnaphylaxisIM, epiArrestIV: epiArrestIV, nrpEpiIV: nrpEpiIV, rsiKetamine: rsiKetamine, rsiRocuronium: rsiRocuronium, rsiSuccinylcholine: rsiSuccinylcholine, minSBP: minSBP, ettSize: ettSize, lundBrowderTBSA: lundBrowderTBSA, LB_BRACKETS: LB_BRACKETS, LB: LB }; if (typeof window !== 'undefined') window._PED_MATH = API; if (typeof module !== 'undefined' && module.exports) module.exports = API; })();