Functional Weave
Code in TypeScript

health.news2@1.0.0

impl/typescript.ts

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) {
    throw new 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) return 3;
  if (rate <= 11) return 1;
  if (rate <= 20) return 0;
  if (rate <= 24) return 2;
  return 3;
}

/**
 * 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) return 3;
    if (spo2 <= 93) return 2;
    if (spo2 <= 95) return 1;
    return 0;
  }
  if (spo2 <= 83) return 3;
  if (spo2 <= 85) return 2;
  if (spo2 <= 87) return 1;
  if (spo2 <= 92 || !onOxygen) return 0;
  if (spo2 <= 94) return 1;
  if (spo2 <= 96) return 2;
  return 3;
}

function systolicScore(sbp: number): number {
  if (sbp <= 90) return 3;
  if (sbp <= 100) return 2;
  if (sbp <= 110) return 1;
  if (sbp <= 219) return 0;
  return 3;
}

function pulseScore(pulse: number): number {
  if (pulse <= 40) return 3;
  if (pulse <= 50) return 1;
  if (pulse <= 90) return 0;
  if (pulse <= 110) return 1;
  if (pulse <= 130) return 2;
  return 3;
}

/** 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) return 3;
  if (t <= 36.0) return 1;
  if (t <= 38.0) return 0;
  if (t <= 39.0) return 1;
  return 2;
}

/**
 * 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.
 */
export function 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) {
    throw new RangeError(`spo2Scale must be 1 or 2, received ${o.spo2Scale}`);
  }
  if (typeof o.onOxygen !== "boolean") {
    throw new 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)) {
    throw new 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) {
    throw new 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";
  else if (total >= 5) clinicalRisk = "medium";
  else if (redScore) clinicalRisk = "low-medium";
  return { total, respirationRate, spo2, airOrOxygen, systolicBp, pulse, consciousness, temperature, redScore, clinicalRisk };
}