use chrono::{Local, TimeZone, Timelike};
use crate::nightscout::Entry;
pub const BUCKETS: usize = 96;
pub const BUCKET_MIN: i64 = 24 * 60 / BUCKETS as i64;
#[derive(Debug, Clone, Copy)]
pub struct Band {
pub minute: i64,
pub p05: f64,
pub p25: f64,
pub p50: f64,
pub p75: f64,
pub p95: f64,
}
pub fn profile(entries: &[Entry]) -> Vec<Band> {
let mut buckets: Vec<Vec<f64>> = vec![Vec::new(); BUCKETS];
for e in entries {
if let Some(dt) = Local.timestamp_millis_opt(e.date).single() {
let minute = dt.hour() as i64 * 60 + dt.minute() as i64;
let idx = (minute / BUCKET_MIN).clamp(0, BUCKETS as i64 - 1) as usize;
buckets[idx].push(e.sgv);
}
}
let mut out = Vec::new();
for (i, vals) in buckets.into_iter().enumerate() {
if vals.is_empty() {
continue;
}
let mut v = vals;
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
out.push(Band {
minute: i as i64 * BUCKET_MIN + BUCKET_MIN / 2,
p05: percentile(&v, 0.05),
p25: percentile(&v, 0.25),
p50: percentile(&v, 0.50),
p75: percentile(&v, 0.75),
p95: percentile(&v, 0.95),
});
}
out
}
fn percentile(sorted: &[f64], q: f64) -> f64 {
match sorted.len() {
0 => 0.0,
1 => sorted[0],
n => {
let rank = q * (n - 1) as f64;
let lo = rank.floor() as usize;
let hi = rank.ceil() as usize;
sorted[lo] + (sorted[hi] - sorted[lo]) * (rank - lo as f64)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(sgv: f64, date: i64) -> Entry {
Entry {
sgv,
date,
direction: None,
}
}
fn at(day: i64, minutes: i64) -> i64 {
let base = Local
.with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
.single()
.unwrap()
.timestamp_millis();
base + (day * 24 * 60 + minutes) * 60_000
}
#[test]
fn empty_profile_for_no_entries() {
assert!(profile(&[]).is_empty());
}
#[test]
fn folds_days_onto_one_clock() {
let e = [
entry(100.0, at(0, 480)), entry(120.0, at(1, 480)), entry(140.0, at(2, 480)), ];
let bands = profile(&e);
assert_eq!(bands.len(), 1);
let b = bands[0];
assert_eq!(b.minute, 480 + BUCKET_MIN / 2);
assert_eq!(b.p50, 120.0); assert!(b.p05 <= b.p25 && b.p25 <= b.p50 && b.p50 <= b.p75 && b.p75 <= b.p95);
}
#[test]
fn separate_times_make_separate_bands() {
let e = [entry(90.0, at(0, 60)), entry(200.0, at(0, 720))];
let bands = profile(&e);
assert_eq!(bands.len(), 2);
assert!(bands[0].minute < bands[1].minute);
}
#[test]
fn percentile_interpolates() {
let v = [10.0, 20.0, 30.0, 40.0];
assert_eq!(percentile(&v, 0.0), 10.0);
assert_eq!(percentile(&v, 1.0), 40.0);
assert_eq!(percentile(&v, 0.5), 25.0); }
}