4,270 bytes · the TypeScript implementation · view raw
import { type Acvpu, type News2Observations, type News2Risk, type News2Score } from"./health_news2_types.ts";
const CONSCIOUSNESS: readonly Acvpu[] = ["alert", "confusion", "voice", "pain", "unresponsive"];
/** Ranges are guard rails against unit and typing mistakes, not RCP rules; out of range is an error, never clamped. */function checkInt(name: string, value: number, low: number, high: number): void {
if (typeof value !== "number" || !Number.isInteger(value) || value < low || value > high) {
thrownew RangeError(`${name} must be a whole number from ${low} to ${high}, received ${value}`);
}
}
/** RCP NEWS2 chart 1, respiration rate per minute. */function respirationScore(rate: number): number {
if (rate <= 8) return3;
if (rate <= 11) return1;
if (rate <= 20) return0;
if (rate <= 24) return2;
return3;
}
/** * Chart 1, SpO2. Scale 2 targets 88-92%, so on oxygen a high saturation * scores (it signals over-oxygenation risk in hypercapnic failure); on air it * does not. Scale 1 ignores oxygen here: that is scored separately. */function spo2Score(spo2: number, scale: number, onOxygen: boolean): number {
if (scale === 1) {
if (spo2 <= 91) return3;
if (spo2 <= 93) return2;
if (spo2 <= 95) return1;
return0;
}
if (spo2 <= 83) return3;
if (spo2 <= 85) return2;
if (spo2 <= 87) return1;
if (spo2 <= 92 || !onOxygen) return0;
if (spo2 <= 94) return1;
if (spo2 <= 96) return2;
return3;
}
function systolicScore(sbp: number): number {
if (sbp <= 90) return3;
if (sbp <= 100) return2;
if (sbp <= 110) return1;
if (sbp <= 219) return0;
return3;
}
function pulseScore(pulse: number): number {
if (pulse <= 40) return3;
if (pulse <= 50) return1;
if (pulse <= 90) return0;
if (pulse <= 110) return1;
if (pulse <= 130) return2;
return3;
}
/** Chart rows are to one decimal place; a value between rows scores with the higher row ("at or below"). */function temperatureScore(t: number): number {
if (t <= 35.0) return3;
if (t <= 36.0) return1;
if (t <= 38.0) return0;
if (t <= 39.0) return1;
return2;
}
/** * The NEWS2 score (Royal College of Physicians, 2017) for one set of adult * observations: each parameter scored from chart 1, plus 2 for supplemental * oxygen, summed; and the clinical risk from chart 2, where a single * parameter scoring 3 raises a low aggregate to low-medium. */exportfunction news2Score(observations: News2Observations): News2Score {
const o = observations;
checkInt("respirationRate", o.respirationRate, 1, 80);
checkInt("spo2", o.spo2, 1, 100);
if (o.spo2Scale !== 1 && o.spo2Scale !== 2) {
thrownew RangeError(`spo2Scale must be 1 or 2, received ${o.spo2Scale}`);
}
if (typeof o.onOxygen !== "boolean") {
thrownew TypeError(`onOxygen must be true or false, received ${o.onOxygen}`);
}
checkInt("systolicBp", o.systolicBp, 20, 300);
checkInt("pulse", o.pulse, 1, 300);
if (!CONSCIOUSNESS.includes(o.consciousness)) {
thrownew RangeError(`consciousness must be one of alert, confusion, voice, pain, unresponsive, received ${o.consciousness}`);
}
if (typeof o.temperature !== "number" || !Number.isFinite(o.temperature) || o.temperature < 25 || o.temperature > 45) {
thrownew RangeError(`temperature must be from 25.0 to 45.0 degrees Celsius, received ${o.temperature}`);
}
const respirationRate = respirationScore(o.respirationRate);
const spo2 = spo2Score(o.spo2, o.spo2Scale, o.onOxygen);
const airOrOxygen = o.onOxygen ? 2 : 0;
const systolicBp = systolicScore(o.systolicBp);
const pulse = pulseScore(o.pulse);
const consciousness = o.consciousness === "alert" ? 0 : 3;
const temperature = temperatureScore(o.temperature);
const total = respirationRate + spo2 + airOrOxygen + systolicBp + pulse + consciousness + temperature;
const redScore = [respirationRate, spo2, systolicBp, pulse, consciousness, temperature].includes(3);
let clinicalRisk: News2Risk = "low";
if (total >= 7) clinicalRisk = "high";
elseif (total >= 5) clinicalRisk = "medium";
elseif (redScore) clinicalRisk = "low-medium";
return { total, respirationRate, spo2, airOrOxygen, systolicBp, pulse, consciousness, temperature, redScore, clinicalRisk };
}