use crate::alert::Alert;
use crate::config::Alerts;
use crate::nightscout::Entry;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tir {
pub very_low: f64,
pub low: f64,
pub in_range: f64,
pub high: f64,
pub very_high: f64,
}
impl Tir {
pub fn below(&self) -> f64 {
self.very_low + self.low
}
pub fn above(&self) -> f64 {
self.high + self.very_high
}
}
pub fn tir(
entries: &[Entry],
urgent_low: f64,
low: f64,
high: f64,
urgent_high: f64,
) -> Option<Tir> {
if entries.is_empty() {
return None;
}
let alerts = Alerts {
urgent_low,
low,
high,
urgent_high,
..Alerts::default()
};
let total = entries.len() as f64;
let (mut vlo, mut lo, mut hi, mut vhi) = (0.0, 0.0, 0.0, 0.0);
for e in entries {
match crate::alert::from_value(e.sgv, &alerts) {
Alert::UrgentLow => vlo += 1.0,
Alert::Low => lo += 1.0,
Alert::High => hi += 1.0,
Alert::UrgentHigh => vhi += 1.0,
Alert::InRange | Alert::Stale => {}
}
}
let pct = |n: f64| n / total * 100.0;
let (very_low, low_pct, high_pct, very_high) = (pct(vlo), pct(lo), pct(hi), pct(vhi));
Some(Tir {
very_low,
low: low_pct,
high: high_pct,
very_high,
in_range: 100.0 - very_low - low_pct - high_pct - very_high,
})
}
pub fn mean_mgdl(entries: &[Entry]) -> Option<f64> {
if entries.is_empty() {
return None;
}
let sum: f64 = entries.iter().map(|e| e.sgv).sum();
Some(sum / entries.len() as f64)
}
pub fn cv_pct(entries: &[Entry]) -> Option<f64> {
if entries.len() < 2 {
return None;
}
let mean = mean_mgdl(entries)?;
if mean <= 0.0 {
return None;
}
let n = entries.len() as f64;
let var = entries.iter().map(|e| (e.sgv - mean).powi(2)).sum::<f64>() / (n - 1.0);
Some(var.sqrt() / mean * 100.0)
}
pub fn gmi(mean_mgdl: f64) -> f64 {
3.31 + 0.02392 * mean_mgdl
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bands_agree_with_the_alarm_at_every_boundary() {
let (ul, lo, hi, uh) = (55.0, 70.0, 180.0, 250.0);
for step in 0..=600 {
let sgv = step as f64;
let t = tir(&[e(sgv)], ul, lo, hi, uh).unwrap();
let band = if t.very_low > 0.0 {
Alert::UrgentLow
} else if t.low > 0.0 {
Alert::Low
} else if t.high > 0.0 {
Alert::High
} else if t.very_high > 0.0 {
Alert::UrgentHigh
} else {
Alert::InRange
};
let alarm = crate::alert::from_value(
sgv,
&Alerts {
urgent_low: ul,
low: lo,
high: hi,
urgent_high: uh,
..Alerts::default()
},
);
assert_eq!(band, alarm, "band disagrees with the alarm at {sgv} mg/dL");
}
}
fn e(sgv: f64) -> Entry {
Entry {
sgv,
date: 0,
direction: None,
}
}
fn tir_default(entries: &[Entry]) -> Option<Tir> {
tir(entries, 55.0, 70.0, 180.0, 250.0)
}
#[test]
fn tir_none_when_empty() {
assert!(tir_default(&[]).is_none());
}
#[test]
fn tir_splits_five_bands() {
let entries = [e(45.0), e(60.0), e(100.0), e(120.0), e(200.0), e(300.0)];
let t = tir_default(&entries).unwrap();
assert!((t.very_low - 100.0 / 6.0).abs() < 1e-9);
assert!((t.low - 100.0 / 6.0).abs() < 1e-9);
assert!((t.in_range - 200.0 / 6.0).abs() < 1e-9);
assert!((t.high - 100.0 / 6.0).abs() < 1e-9);
assert!((t.very_high - 100.0 / 6.0).abs() < 1e-9);
assert!((t.below() - 200.0 / 6.0).abs() < 1e-9);
let total = t.very_low + t.low + t.in_range + t.high + t.very_high;
assert!((total - 100.0).abs() < 1e-9);
}
#[test]
fn tir_bands_match_the_alert_thresholds() {
assert_eq!(tir_default(&[e(55.0)]).unwrap().very_low, 100.0);
assert_eq!(tir_default(&[e(69.0)]).unwrap().low, 100.0);
assert_eq!(tir_default(&[e(70.0)]).unwrap().in_range, 100.0);
assert_eq!(tir_default(&[e(180.0)]).unwrap().in_range, 100.0);
assert_eq!(tir_default(&[e(181.0)]).unwrap().high, 100.0);
assert_eq!(tir_default(&[e(250.0)]).unwrap().very_high, 100.0);
}
#[test]
fn mean_and_gmi() {
assert_eq!(mean_mgdl(&[e(100.0), e(200.0)]).unwrap(), 150.0);
assert!((gmi(150.0) - 6.898).abs() < 0.01);
}
#[test]
fn cv_needs_two_readings_and_scales_with_spread() {
assert!(cv_pct(&[]).is_none());
assert!(cv_pct(&[e(100.0)]).is_none());
assert_eq!(cv_pct(&[e(100.0), e(100.0)]).unwrap(), 0.0);
let cv = cv_pct(&[e(90.0), e(110.0)]).unwrap();
assert!((cv - 14.14).abs() < 0.01, "cv was {cv}");
assert!(cv_pct(&[e(50.0), e(150.0)]).unwrap() > cv);
}
}