Functional Weave
Code in Rust

health.news2@1.0.0

impl/rust.rs

6,725 bytes · the Rust implementation · view raw

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