health.news2 Unreviewed
NEWS2 early warning score from one set of observations: parameter scores, aggregate and RCP clinical risk band.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 45 tests, run in TypeScript, Python and Rust.
Unreviewed. Not for clinical use; regulatory review pending. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified acute-care clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have an acute-care clinician review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
Not a medical device. It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software’s regulatory status, and must validate it under their own clinical governance.
What it does
Status: needs review and sign-off by a qualified clinician before it is published. Not a medical device; for decision support only; always follow local clinical guidelines.
The National Early Warning Score 2 for one set of adult observations. Each of the six physiological parameters is scored from the Royal College of Physicians' chart 1, 2 is added when the patient needs supplemental oxygen, and the sum is the aggregate NEW score. The clinical risk band comes from chart 2:
For example
news2_score(respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37)→ respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 0, red score false, clinical risk low normal observations score 0, low risknews2_score(respiration rate 8, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37)→ respiration rate 3, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium respiration 8 is a red 3: aggregate 3 but low-medium risknews2_score(respiration rate 11, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37)→ respiration rate 1, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 1, red score false, clinical risk low respiration 11 scores 1
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn news2_score(observations: &News2Observations) -> News2Score
| observations | News2Observations | one set of adult observations taken together |
| returns | News2Score | each parameter's score, the aggregate, whether any single parameter scored 3, and the risk band |
The types it declares, generated into your project
/// One set of observations, as recorded on the NEWS2 chart.
#[derive(Debug, Clone, PartialEq)]
pub struct News2Observations {
/// breaths per minute, 1 to 80
pub respiration_rate: i64,
/// oxygen saturation in percent, 1 to 100
pub spo2: i64,
/// 1, or 2 only for confirmed hypercapnic respiratory failure on a clinician's direction
pub spo2_scale: i64,
/// true when supplemental oxygen is needed to keep the prescribed saturation
pub on_oxygen: bool,
/// mmHg, 20 to 300
pub systolic_bp: i64,
/// beats per minute, 1 to 300
pub pulse: i64,
/// ACVPU: alert, new confusion, voice, pain or unresponsive
pub consciousness: String,
/// degrees Celsius, 25.0 to 45.0
pub temperature: f64,
}
// Acvpu is a string in Rust, one of: "alert", "confusion", "voice", "pain", "unresponsive".
// Parameters take it as &str and results hold it as String.
// News2Risk is a string in Rust, one of: "low", "low-medium", "medium", "high".
// Parameters take it as &str and results hold it as String.
/// The chart's scores for each parameter, their sum and the clinical risk it triggers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct News2Score {
/// aggregate NEW score, 0 to 20
pub total: i64,
pub respiration_rate: i64,
pub spo2: i64,
/// 2 on supplemental oxygen, else 0
pub air_or_oxygen: i64,
pub systolic_bp: i64,
pub pulse: i64,
pub consciousness: i64,
pub temperature: i64,
/// true when any single parameter scored 3
pub red_score: bool,
/// RCP chart 2: low, low-medium (a red score), medium or high
pub clinical_risk: String,
}
Your code names it in one line, in the file that uses it
fune!(health.news2@^1); // then call news2_score(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
const CONSCIOUSNESS: [&str; 5] = ["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.
fn check_int(name: &str, value: i64, low: i64, high: i64) {
if value < low || value > high {
panic!("{} must be a whole number from {} to {}, received {}", name, low, high, value);
}
}
/// RCP NEWS2 chart 1, respiration rate per minute.
fn respiration_score(rate: i64) -> i64 {
match rate {
i64::MIN..=8 => 3,
9..=11 => 1,
12..=20 => 0,
21..=24 => 2,
_ => 3,
}
}
/// Chart 1, SpO2. Scale 2 targets 88-92%, so on oxygen a high saturation
/// scores; on air it does not. Scale 1 ignores oxygen here: that is scored
/// separately.
fn spo2_score(spo2: i64, scale: i64, on_oxygen: bool) -> i64 {
if scale == 1 {
return match spo2 {
i64::MIN..=91 => 3,
92..=93 => 2,
94..=95 => 1,
_ => 0,
};
}
match spo2 {
i64::MIN..=83 => 3,
84..=85 => 2,
86..=87 => 1,
88..=92 => 0,
_ if !on_oxygen => 0,
93..=94 => 1,
95..=96 => 2,
_ => 3,
}
}
fn systolic_score(sbp: i64) -> i64 {
match sbp {
i64::MIN..=90 => 3,
91..=100 => 2,
101..=110 => 1,
111..=219 => 0,
_ => 3,
}
}
fn pulse_score(pulse: i64) -> i64 {
match pulse {
i64::MIN..=40 => 3,
41..=50 => 1,
51..=90 => 0,
91..=110 => 1,
111..=130 => 2,
_ => 3,
}
}
/// Chart rows are to one decimal place; a value between rows scores with the
/// higher row ("at or below").
fn temperature_score(t: f64) -> i64 {
if t <= 35.0 {
3
} else if t <= 36.0 {
1
} else if t <= 38.0 {
0
} else if t <= 39.0 {
1
} else {
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.
///
/// # Panics
/// Panics when an observation is outside its range or consciousness is not
/// one of the ACVPU values.
pub fn news2_score(observations: &News2Observations) -> News2Score {
let o = observations;
check_int("respirationRate", o.respiration_rate, 1, 80);
check_int("spo2", o.spo2, 1, 100);
if o.spo2_scale != 1 && o.spo2_scale != 2 {
panic!("spo2Scale must be 1 or 2, received {}", o.spo2_scale);
}
check_int("systolicBp", o.systolic_bp, 20, 300);
check_int("pulse", o.pulse, 1, 300);
if !CONSCIOUSNESS.contains(&o.consciousness.as_str()) {
panic!(
"consciousness must be one of alert, confusion, voice, pain, unresponsive, received {}",
o.consciousness
);
}
if !o.temperature.is_finite() || o.temperature < 25.0 || o.temperature > 45.0 {
panic!("temperature must be from 25.0 to 45.0 degrees Celsius, received {}", o.temperature);
}
let respiration_rate = respiration_score(o.respiration_rate);
let spo2 = spo2_score(o.spo2, o.spo2_scale, o.on_oxygen);
let air_or_oxygen = if o.on_oxygen { 2 } else { 0 };
let systolic_bp = systolic_score(o.systolic_bp);
let pulse = pulse_score(o.pulse);
let consciousness = if o.consciousness == "alert" { 0 } else { 3 };
let temperature = temperature_score(o.temperature);
let total = respiration_rate + spo2 + air_or_oxygen + systolic_bp + pulse + consciousness + temperature;
let red_score = [respiration_rate, spo2, systolic_bp, pulse, consciousness, temperature].contains(&3);
let clinical_risk = if total >= 7 {
"high"
} else if total >= 5 {
"medium"
} else if red_score {
"low-medium"
} else {
"low"
};
News2Score {
total,
respiration_rate,
spo2,
air_or_oxygen,
systolic_bp,
pulse,
consciousness,
temperature,
red_score,
clinical_risk: clinical_risk.to_string(),
}
}
pub fn news2_score_to_value(score: &News2Score) -> Value {
Value::obj(vec![
("total", Value::Int(score.total)),
("respirationRate", Value::Int(score.respiration_rate)),
("spo2", Value::Int(score.spo2)),
("airOrOxygen", Value::Int(score.air_or_oxygen)),
("systolicBp", Value::Int(score.systolic_bp)),
("pulse", Value::Int(score.pulse)),
("consciousness", Value::Int(score.consciousness)),
("temperature", Value::Int(score.temperature)),
("redScore", Value::Bool(score.red_score)),
("clinicalRisk", Value::str(&score.clinical_risk)),
])
}
/// An integer field, refusing what the typed struct cannot hold (a fraction,
/// a string) with the wording TypeScript and Python use.
fn int_field(v: &Value, name: &str, low: i64, high: i64) -> i64 {
match v {
Value::Int(i) => *i,
Value::Float(f) if f.fract() == 0.0 && f.is_finite() => *f as i64,
_ => panic!("{} must be a whole number from {} to {}, received {:?}", name, low, high, v),
}
}
pub fn news2_observations_from_value(v: &Value) -> News2Observations {
let spo2_scale = match v.get("spo2Scale") {
Value::Int(i) => *i,
other => panic!("spo2Scale must be 1 or 2, received {:?}", other),
};
let on_oxygen = match v.get("onOxygen") {
Value::Bool(b) => *b,
other => panic!("onOxygen must be true or false, received {:?}", other),
};
let temperature = match v.get("temperature") {
Value::Int(_) | Value::Float(_) => v.get("temperature").as_f64(),
other => panic!("temperature must be from 25.0 to 45.0 degrees Celsius, received {:?}", other),
};
let consciousness = match v.get("consciousness") {
Value::Str(s) => s.clone(),
other => panic!(
"consciousness must be one of alert, confusion, voice, pain, unresponsive, received {:?}",
other
),
};
News2Observations {
respiration_rate: int_field(v.get("respirationRate"), "respirationRate", 1, 80),
spo2: int_field(v.get("spo2"), "spo2", 1, 100),
spo2_scale,
on_oxygen,
systolic_bp: int_field(v.get("systolicBp"), "systolicBp", 20, 300),
pulse: int_field(v.get("pulse"), "pulse", 1, 300),
consciousness,
temperature,
}
}
pub fn fune_vector(args: &[Value]) -> Value {
news2_score_to_value(&news2_score(&news2_observations_from_value(&args[0])))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add health.news2
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./health.news2-1.0.1-rust.fune, or fetch it from a terminal with fune pull health.news2@1.0.1:rust.
The whole function, every language, is one file too: health.news2-1.0.1.fune, 50,450 bytes, sha256 ff104d3b779d004108990f2752a5f6c606cac001472954b3ed60b0cdd256246f. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before health.news2
after — your function gets the result and the arguments, and returns the final result.
// fune: after health.news2
replace — it requires no other capability, so there is no dependency to replace.
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show health.news2 --steps.
// fune: step health.news2 after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| normal observations score 0, low risk | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 0, red score false, clinical risk low |
| respiration 8 is a red 3: aggregate 3 but low-medium risk | respiration rate 8, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 3, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| respiration 11 scores 1 | respiration rate 11, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 1, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 1, red score false, clinical risk low |
| respiration 21 scores 2, 24 still 2 | respiration rate 24, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 2, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| respiration 25 scores 3 | respiration rate 25, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 3, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| the low edge of every 1-point row: 9/min, SpO2 94, SBP 101, pulse 41, 35.1C make 5, medium without any red | respiration rate 9, spo2 94, spo2 scale 1, on oxygen false, systolic bp 101, pulse 41, consciousness alert, temperature 35.1 | → | respiration rate 1, spo2 1, air or oxygen 0, systolic bp 1, pulse 1, consciousness 0, temperature 1, total 5, red score false, clinical risk medium |
| the edges of the 0 rows: 12 and 20/min, SpO2 96, SBP 111, pulse 51, 36.1C all score 0 | respiration rate 12, spo2 96, spo2 scale 1, on oxygen false, systolic bp 111, pulse 51, consciousness alert, temperature 36.1 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 0, red score false, clinical risk low |
| the other edges of the 0 rows: 20/min, SBP 219, pulse 90, 38.0C | respiration rate 20, spo2 97, spo2 scale 1, on oxygen false, systolic bp 219, pulse 90, consciousness alert, temperature 38 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 0, red score false, clinical risk low |
| scale 1: SpO2 91 is a red 3 | respiration rate 16, spo2 91, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 3, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| scale 1: SpO2 92-93 score 2 (93) | respiration rate 16, spo2 93, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 2, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
Show the other 35 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| scale 1: SpO2 97 on oxygen scores 0 for saturation plus 2 for oxygen | respiration rate 16, spo2 97, spo2 scale 1, on oxygen true, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 2, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| scale 2 on air: SpO2 97 scores 0 | respiration rate 16, spo2 97, spo2 scale 2, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 0, red score false, clinical risk low |
| scale 2 on oxygen: SpO2 97 scores 3 plus 2 for oxygen, medium (scale 1 would give 2, low) | respiration rate 16, spo2 97, spo2 scale 2, on oxygen true, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 3, air or oxygen 2, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 5, red score true, clinical risk medium |
| scale 2 on oxygen: SpO2 93-94 scores 1 (93) | respiration rate 16, spo2 93, spo2 scale 2, on oxygen true, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 1, air or oxygen 2, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score false, clinical risk low |
| scale 2 on oxygen: SpO2 95-96 scores 2 (96) | respiration rate 16, spo2 96, spo2 scale 2, on oxygen true, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 2, air or oxygen 2, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 4, red score false, clinical risk low |
| scale 2: SpO2 88-92 is the target range and scores 0 even on oxygen (88) | respiration rate 16, spo2 88, spo2 scale 2, on oxygen true, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 2, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| scale 2: SpO2 86 scores 1 | respiration rate 16, spo2 86, spo2 scale 2, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 1, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 1, red score false, clinical risk low |
| scale 2: SpO2 84 scores 2 | respiration rate 16, spo2 84, spo2 scale 2, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 2, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| scale 2: SpO2 83 is a red 3 | respiration rate 16, spo2 83, spo2 scale 2, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 3, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| systolic 220 is a red 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 220, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 3, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| systolic 90 is a red 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 90, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 3, pulse 0, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| systolic 100 scores 2 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 100, pulse 70, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 2, pulse 0, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| pulse 40 scores 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 40, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 3, consciousness 0, temperature 0, total 3, red score true, clinical risk low-medium |
| pulse 91 scores 1 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 91, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 1, consciousness 0, temperature 0, total 1, red score false, clinical risk low |
| pulse 111-130 scores 2 (130) | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 130, consciousness alert, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 2, consciousness 0, temperature 0, total 2, red score false, clinical risk low |
| pulse 131 scores 3 and with respiration 21 (2) the aggregate is 5: medium outranks the red score | respiration rate 21, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 131, consciousness alert, temperature 37 | → | respiration rate 2, spo2 0, air or oxygen 0, systolic bp 0, pulse 3, consciousness 0, temperature 0, total 5, red score true, clinical risk medium |
| new confusion alone is a red 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness confusion, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 3, temperature 0, total 3, red score true, clinical risk low-medium |
| responds to voice scores 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness voice, temperature 37 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 3, temperature 0, total 3, red score true, clinical risk low-medium |
| temperature 35.0 scores 3 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 35 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 3, total 3, red score true, clinical risk low-medium |
| temperature 36.0 scores 1 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 36 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 1, total 1, red score false, clinical risk low |
| temperature 38.1 scores 1 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 38.1 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 1, total 1, red score false, clinical risk low |
| temperature 39.0 scores 1 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 39 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 1, total 1, red score false, clinical risk low |
| temperature 39.1 scores 2 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 39.1 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 2, total 2, red score false, clinical risk low |
| a temperature between chart rows, 35.05, scores with the higher row: 1 | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 35.05 | → | respiration rate 0, spo2 0, air or oxygen 0, systolic bp 0, pulse 0, consciousness 0, temperature 1, total 1, red score false, clinical risk low |
| aggregate 4 with no red score is low | respiration rate 22, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 115, consciousness alert, temperature 37 | → | respiration rate 2, spo2 0, air or oxygen 0, systolic bp 0, pulse 2, consciousness 0, temperature 0, total 4, red score false, clinical risk low |
| aggregate 7 exactly is high | respiration rate 22, spo2 97, spo2 scale 1, on oxygen false, systolic bp 95, pulse 115, consciousness alert, temperature 38.5 | → | respiration rate 2, spo2 0, air or oxygen 0, systolic bp 2, pulse 2, consciousness 0, temperature 1, total 7, red score false, clinical risk high |
| a septic picture: 25/min, SpO2 93 on oxygen, SBP 105, pulse 115, 38.5C, unresponsive | respiration rate 25, spo2 93, spo2 scale 1, on oxygen true, systolic bp 105, pulse 115, consciousness unresponsive, temperature 38.5 | → | respiration rate 3, spo2 2, air or oxygen 2, systolic bp 1, pulse 2, consciousness 3, temperature 1, total 14, red score true, clinical risk high |
| a pulse above 300 is an error, not clamped | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 301, consciousness alert, temperature 37 | → | error: pulse must be a whole number from 1 to 300 |
| a respiration rate of 0 is an error | respiration rate 0, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | error: respirationRate must be a whole number from 1 to 80 |
| a fractional respiration rate is an error | respiration rate 16.5, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | error: respirationRate must be a whole number from 1 to 80 |
| an SpO2 above 100 is an error | respiration rate 16, spo2 101, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | error: spo2 must be a whole number from 1 to 100 |
| there is no SpO2 scale 3 | respiration rate 16, spo2 97, spo2 scale 3, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 37 | → | error: spo2Scale must be 1 or 2 |
| a temperature in Fahrenheit is an error, not a fever | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness alert, temperature 98.6 | → | error: temperature must be from 25.0 to 45.0 degrees Celsius |
| consciousness outside ACVPU is an error | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 120, pulse 70, consciousness drowsy, temperature 37 | → | error: consciousness must be one of alert, confusion, voice, pain, unresponsive |
| a systolic pressure of 10 is an error | respiration rate 16, spo2 97, spo2 scale 1, on oxygen false, systolic bp 10, pulse 70, consciousness alert, temperature 37 | → | error: systolicBp must be a whole number from 20 to 300 |
More from the author
| NEW score | Clinical risk | |---|---| | aggregate 0-4 | low | | a score of 3 in any single parameter (a "red score") | low-medium | | aggregate 5-6 | medium | | aggregate 7 or more | high |
The bands are checked in the order high, medium, red score, low: a red score raises a low aggregate to low-medium, but an aggregate of 5 or more already outranks it. `redScore` is returned separately because the RCP asks for an urgent ward-based review for any red score, whatever the band. The oxygen weighting (2) can never be a red score on its own.
## The scoring (RCP chart 1)
| Parameter | 3 | 2 | 1 | 0 | 1 | 2 | 3 | |---|---|---|---|---|---|---|---| | Respiration rate /min | ≤8 | | 9-11 | 12-20 | | 21-24 | ≥25 | | SpO2 scale 1 % | ≤91 | 92-93 | 94-95 | ≥96 | | | | | SpO2 scale 2 % | ≤83 | 84-85 | 86-87 | 88-92, or ≥93 on air | 93-94 on oxygen | 95-96 on oxygen | ≥97 on oxygen | | Air or oxygen | | oxygen | | air | | | | | Systolic BP mmHg | ≤90 | 91-100 | 101-110 | 111-219 | | | ≥220 | | Pulse /min | ≤40 | | 41-50 | 51-90 | 91-110 | 111-130 | ≥131 | | Consciousness | | | | alert | | | new confusion, voice, pain, unresponsive | | Temperature °C | ≤35.0 | | 35.1-36.0 | 36.1-38.0 | 38.1-39.0 | ≥39.1 | |
## Things a caller must get right
- **SpO2 scale 2 is only for patients with confirmed hypercapnic (type 2) respiratory failure, on the direction of a competent clinical decision maker** (RCP report, paragraphs 25-26). On scale 2 a high saturation while on oxygen scores up to 3, because over-oxygenation is the danger; on air it scores 0. Scoring such a patient on scale 1 under-scores them. - **NEWS2 is for adults aged 16 or over and not in pregnancy** (RCP report, paragraph 2). This function cannot see age or pregnancy; the caller must not use it for children or pregnant women. - **New confusion** (`confusion`, the C of ACVPU) means new-onset confusion, disorientation or delirium, and scores 3 like voice, pain or unresponsive. - **Temperature** is compared "at or below" each chart row, so a value between two one-decimal rows (35.05) scores with the higher row (35.1-36.0, score 1). Thermometers record one decimal place. - The other observations are whole numbers, as charted.
## Input guard rails (not from the RCP)
Out-of-range values are errors, never clamped, so a unit or typing mistake (98.6 for a Celsius temperature, a pulse of 700) cannot quietly produce a score. The ranges are the implementation's own and need clinical sign-off: respiration rate 1-80 /min, SpO2 1-100 %, systolic BP 20-300 mmHg, pulse 1-300 /min, temperature 25.0-45.0 °C, SpO2 scale 1 or 2, consciousness one of `alert`, `confusion`, `voice`, `pain`, `unresponsive`.
## Source
Royal College of Physicians. *National Early Warning Score (NEWS) 2: Standardising the assessment of acute-illness severity in the NHS.* Updated report of a working party. London: RCP, 2017. Chart 1 "The NEWS scoring system" and chart 2 "NEWS thresholds and triggers", https://www.rcp.ac.uk/improving-care/resources/national-early-warning-score-news-2/ . The vectors were worked out by hand from those two charts; the RCP publishes no numeric worked examples.
## Before you rely on this
**Not professional advice.** This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have an acute-care clinician review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Not a medical device.** It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software's regulatory status, and must validate it under their own clinical governance.
**Not for clinical use; regulatory review pending.** Whether publishing this capability makes it a medical device is under regulatory review. Until that is settled it is a developer library only.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified acute-care clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
## Notices
Reproduced from: Royal College of Physicians. National Early Warning Score (NEWS) 2: Standardising the assessment of acute-illness severity in the NHS. Updated report of a working party. London: RCP, 2017.
1.0.1 marks it unreviewed (not for clinical use; regulatory review pending) and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 220 |
| README.md | 5,566 |
| impl/python.py | 4,452 |
| impl/rust.rs | 6,725 |
| impl/typescript.ts | 4,270 |
| vectors.json | 20,819 |