use crate::nightscout::{Entry, Prediction};
const BG_REF: f64 = 140.0;
const AR: [f64; 2] = [-0.723, 1.716];
const STEP_MS: i64 = 5 * 60_000;
const STEPS: usize = 6;
const BG_MIN: f64 = 36.0;
const BG_MAX: f64 = 400.0;
const SPREAD_PER_STEP: f64 = 4.0;
pub fn ar2(entries: &[Entry]) -> Vec<Prediction> {
let (latest, prev) = match (entries.first(), entries.get(1)) {
(Some(a), Some(b)) => (a, b),
_ => return Vec::new(),
};
let mut y0 = (prev.sgv / BG_REF).ln();
let mut y1 = (latest.sgv / BG_REF).ln();
let mut out = Vec::with_capacity(STEPS);
for i in 1..=STEPS as i64 {
let y_next = AR[0] * y0 + AR[1] * y1;
y0 = y1;
y1 = y_next;
let center = BG_REF * y_next.exp();
let spread = SPREAD_PER_STEP * i as f64;
out.push(Prediction {
at_ms: latest.date + i * STEP_MS,
low: (center - spread).clamp(BG_MIN, BG_MAX),
high: (center + spread).clamp(BG_MIN, BG_MAX),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(sgv: f64, date: i64) -> Entry {
Entry {
sgv,
date,
direction: None,
}
}
#[test]
fn empty_without_two_readings() {
assert!(ar2(&[]).is_empty());
assert!(ar2(&[entry(100.0, 0)]).is_empty());
}
#[test]
fn projects_six_widening_bands_five_min_apart() {
let now = 1_000_000_000_000;
let out = ar2(&[entry(120.0, now), entry(115.0, now - STEP_MS)]);
assert_eq!(out.len(), STEPS);
assert_eq!(out[0].at_ms, now + STEP_MS);
assert_eq!(out[5].at_ms, now + 6 * STEP_MS);
let w0 = out[0].high - out[0].low;
let w5 = out[5].high - out[5].low;
assert!(w5 > w0);
let flat = ar2(&[entry(100.0, now), entry(100.0, now - STEP_MS)]);
let mid = (flat[0].low + flat[0].high) / 2.0;
assert!((mid - 100.0).abs() < 1.0);
}
#[test]
fn stays_within_physiological_clamp() {
let now = 0;
let out = ar2(&[entry(390.0, now), entry(300.0, now - STEP_MS)]);
assert!(out
.iter()
.all(|p| (BG_MIN..=BG_MAX).contains(&p.low) && (BG_MIN..=BG_MAX).contains(&p.high)));
}
}