#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::sqrt;
#[derive(Debug, Clone, Copy)]
pub struct Eta {
pub eta: f64,
pub eta_low: f64,
pub eta_high: f64,
pub slope: f64,
}
pub fn time_to_threshold(series: &[f64], fit_window: usize, threshold: f64) -> Option<Eta> {
let n = series.len();
if n < fit_window || fit_window < 8 {
return None;
}
let w = &series[n - fit_window..];
let m = fit_window as f64;
let sx = m * (m - 1.0) / 2.0;
let sx2 = m * (m - 1.0) * (2.0 * m - 1.0) / 6.0;
let sy: f64 = w.iter().sum();
let sxy: f64 = w.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
let det = m * sx2 - sx * sx;
if det.abs() < 1e-12 {
return None;
}
let a = (sx2 * sy - sx * sxy) / det;
let b = (m * sxy - sx * sy) / det;
let mut ss = 0.0;
for (i, &y) in w.iter().enumerate() {
let r = y - (a + b * i as f64);
ss += r * r;
}
let dof = (m - 2.0).max(1.0);
let s2 = ss / dof;
let mean_x = sx / m;
let sxx = sx2 - m * mean_x * mean_x;
let se_b = sqrt(s2 / sxx.max(1e-12));
let current = a + b * (m - 1.0);
let remaining = threshold - current;
if b.abs() <= se_b || remaining.signum() != b.signum() {
return None;
}
let eta = remaining / b;
let b_lo = b - se_b;
let b_hi = b + se_b;
let mut bounds = [remaining / b_hi, remaining / b_lo];
if bounds[0] > bounds[1] {
bounds.swap(0, 1);
}
Some(Eta { eta, eta_low: bounds[0], eta_high: bounds[1], slope: b })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_ramp_predicts_exactly() {
let series: Vec<f64> = (0..100).map(|i| 0.01 * i as f64).collect();
let eta = time_to_threshold(&series, 50, 2.0).expect("trend exists");
assert!((eta.eta - 101.0).abs() < 1.0, "eta {}", eta.eta);
assert!(eta.eta_low <= eta.eta && eta.eta <= eta.eta_high);
}
#[test]
fn flat_noise_returns_none() {
let mut state = 12345u64;
let series: Vec<f64> = (0..200)
.map(|_| {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
(state >> 33) as f64 / (1u64 << 31) as f64 - 0.5
})
.collect();
assert!(time_to_threshold(&series, 100, 10.0).is_none());
}
#[test]
fn trend_away_from_threshold_returns_none() {
let series: Vec<f64> = (0..100).map(|i| -0.01 * i as f64).collect();
assert!(time_to_threshold(&series, 50, 2.0).is_none());
}
}