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])))
}