use super::funejson::Value; 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]))) }