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 tir(
entries: &[Entry],
urgent_low: f64,
low: f64,
high: f64,
urgent_high: f64,
) -> Option<Tir> {
if entries.is_empty() {
return None;
}
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 {
if e.sgv <= urgent_low {
vlo += 1.0;
} else if e.sgv < low {
lo += 1.0;
} else if e.sgv >= urgent_high {
vhi += 1.0;
} else if e.sgv > high {
hi += 1.0;
}
}
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::*;
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);
}
}