use chrono::{DateTime, Utc};
use crate::{Error, Result, time::posix_nanos, value::Value};
pub struct Knots<'a, V> {
pub t0: i128,
pub h: f64,
pub y: Vec<f64>,
pub offsets: Vec<i128>,
pub originals: Vec<&'a V>,
pub centre: f64,
pub scale: f64,
pub min: f64,
pub max: f64,
}
impl<'a, V: Value> Knots<'a, V> {
pub fn new(timestamps: impl ExactSizeIterator<Item = DateTime<Utc>>, values: impl ExactSizeIterator<Item = &'a V>) -> Result<Self> {
let mut samples: Vec<(i128, DateTime<Utc>, &V)> = timestamps.zip(values).map(|(t, v)| (posix_nanos(t), t, v)).collect();
if samples.is_empty() {
return Err(Error::NoPoints);
}
if let Some(&(_, timestamp, _)) = samples.iter().find(|(_, _, v)| v.to_finite_f64().is_none()) {
return Err(Error::ValueOutOfRange { timestamp });
}
samples.sort_by_key(|&(n, _, _)| n);
let mut distinct: Vec<(i128, DateTime<Utc>, &V)> = Vec::with_capacity(samples.len());
for sample in samples {
match distinct.last_mut() {
Some(last) if last.0 == sample.0 => *last = sample,
_ => distinct.push(sample),
}
}
let mut raw_values = Vec::with_capacity(distinct.len());
for &(_, timestamp, value) in &distinct {
raw_values.push(value.to_finite_f64().ok_or(Error::ValueOutOfRange { timestamp })?);
}
let (min, max) = raw_values.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| (lo.min(v), hi.max(v)));
let half_width = max / 2.0 - min / 2.0;
let scale = if half_width > 0.0 { half_width } else { 1.0 };
let centre = min / 2.0 + max / 2.0;
let t0 = distinct[0].0;
let offsets: Vec<i128> = distinct.iter().map(|&(n, _, _)| n - t0).collect();
let n = offsets.len();
#[allow(clippy::cast_precision_loss)] let h = if n > 1 { offsets[n - 1] as f64 / (n - 1) as f64 } else { 1.0 };
let y = raw_values.iter().map(|&v| (v - centre) / scale).collect();
let originals = distinct.into_iter().map(|(_, _, v)| v).collect();
Ok(Self { t0, h, y, offsets, originals, centre, scale, min, max })
}
pub const fn len(&self) -> usize {
self.offsets.len()
}
pub fn data(&self) -> crate::kernel::Data<'_> {
crate::kernel::Data { offsets: &self.offsets, y: &self.y, inv_h: 1.0 / self.h }
}
pub fn y_range(&self) -> (f64, f64) {
self.y.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| (lo.min(v), hi.max(v)))
}
}
#[cfg(test)]
mod tests {
use bigdecimal::BigDecimal;
use chrono::TimeZone;
use super::*;
fn at(secs: i64) -> DateTime<Utc> {
Utc.timestamp_opt(secs, 0).single().expect("valid")
}
#[test]
fn sorts_and_keeps_the_last_duplicate() {
let ts = [at(20), at(0), at(10), at(10)];
let vs = [2.0, 0.0, 1.0, 9.0];
let knots = Knots::new(ts.into_iter(), vs.iter()).expect("knots");
assert_eq!(knots.len(), 3);
assert_eq!(knots.offsets, vec![0, 10_000_000_000, 20_000_000_000]);
assert_eq!(knots.h, 10_000_000_000.0);
assert_eq!(knots.originals.iter().map(|v| **v).collect::<Vec<_>>(), vec![0.0, 9.0, 2.0]);
let restored: Vec<f64> = knots.y.iter().map(|y| y * knots.scale + knots.centre).collect();
assert_eq!(restored, vec![0.0, 9.0, 2.0]);
}
#[test]
fn constant_and_single_inputs_normalise() {
let knots = Knots::new([at(0), at(5)].into_iter(), [7.0, 7.0].iter()).expect("knots");
assert_eq!(knots.y, vec![0.0, 0.0]);
assert_eq!(knots.centre, 7.0);
let knots = Knots::new([at(0)].into_iter(), [3.0].iter()).expect("knots");
assert_eq!(knots.offsets, vec![0]);
}
#[test]
fn rejects_empty_and_unrepresentable() {
let none: [f64; 0] = [];
assert!(matches!(Knots::new(std::iter::empty(), none.iter()), Err(Error::NoPoints)));
let huge: BigDecimal = "1e999".parse().expect("parse");
assert!(matches!(Knots::new([at(0)].into_iter(), [huge].iter()), Err(Error::ValueOutOfRange { .. })));
assert!(matches!(Knots::new([at(0)].into_iter(), [f64::NAN].iter()), Err(Error::ValueOutOfRange { .. })));
}
#[test]
fn extreme_magnitudes_do_not_overflow_the_scale() {
let knots = Knots::new([at(0), at(1)].into_iter(), [f64::MAX, -f64::MAX].iter()).expect("knots");
assert!(knots.scale.is_finite() && knots.centre.is_finite());
assert_eq!(knots.y, vec![1.0, -1.0]);
}
}