pub mod bin;
pub mod cumulative;
pub mod groupby;
pub mod lttb;
pub mod normalize;
pub mod quantile;
pub mod rolling;
pub mod stack;
pub use bin::{bin, bin_by_count, resolve_bin_count, Bin, BinPolicy};
pub use cumulative::{cumsum, running_max, running_min};
pub use groupby::{rollup, rollup_with, Reducer};
pub use lttb::lttb;
pub use normalize::{normalize, percent_of_total, z_score};
pub use quantile::quantile;
pub use rolling::{ema, rolling_mean, rolling_median, EdgePolicy};
pub use stack::{sort_index_by_value, stack, StackOffset, StackOrder};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissingDataPolicy {
Skip,
Propagate,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransformError {
NonFinite { index: usize },
EmptyInput,
}
impl std::fmt::Display for TransformError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransformError::NonFinite { index } => write!(f, "non-finite value at index {index}"),
TransformError::EmptyInput => write!(f, "no finite values to compute over"),
}
}
}
impl std::error::Error for TransformError {}
pub(crate) fn apply_policy(values: &[f64], policy: MissingDataPolicy) -> Result<Vec<f64>, TransformError> {
match policy {
MissingDataPolicy::Skip => Ok(values.iter().copied().filter(|v| v.is_finite()).collect()),
MissingDataPolicy::Propagate => Ok(values.to_vec()),
MissingDataPolicy::Error => match values.iter().position(|v| !v.is_finite()) {
Some(index) => Err(TransformError::NonFinite { index }),
None => Ok(values.to_vec()),
},
}
}
pub(crate) fn contains_nan(values: &[f64]) -> bool {
values.iter().any(|v| v.is_nan())
}
pub(crate) fn kahan_sum_running(values: &[f64]) -> Vec<f64> {
let mut sum = 0.0_f64;
let mut compensation = 0.0_f64;
let mut out = Vec::with_capacity(values.len());
for &v in values {
let y = v - compensation;
let t = sum + y;
compensation = (t - sum) - y;
sum = t;
out.push(sum);
}
out
}
pub(crate) fn kahan_sum(values: &[f64]) -> f64 {
kahan_sum_running(values).last().copied().unwrap_or(0.0)
}
pub(crate) fn mean_kahan(values: &[f64]) -> f64 {
if values.is_empty() {
return f64::NAN;
}
kahan_sum(values) / values.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_policy_skip_drops_non_finite_values() {
let values = [1.0, f64::NAN, 2.0, f64::INFINITY, 3.0];
let out = apply_policy(&values, MissingDataPolicy::Skip).expect("Skip never errors");
assert_eq!(out, vec![1.0, 2.0, 3.0]);
}
#[test]
fn apply_policy_propagate_returns_values_unchanged() {
let values = [1.0, f64::NAN, 2.0];
let out = apply_policy(&values, MissingDataPolicy::Propagate).expect("Propagate never errors");
assert_eq!(out.len(), 3);
assert!(out[1].is_nan());
}
#[test]
fn apply_policy_error_reports_the_first_non_finite_index() {
let values = [1.0, 2.0, f64::NAN, f64::INFINITY];
let err = apply_policy(&values, MissingDataPolicy::Error).expect_err("must reject a non-finite value");
assert_eq!(err, TransformError::NonFinite { index: 2 });
}
#[test]
fn apply_policy_on_empty_input_never_errors_under_any_policy() {
for policy in [MissingDataPolicy::Skip, MissingDataPolicy::Propagate, MissingDataPolicy::Error] {
assert_eq!(apply_policy(&[], policy), Ok(Vec::new()));
}
}
#[test]
fn contains_nan_ignores_infinities() {
assert!(!contains_nan(&[1.0, f64::INFINITY, f64::NEG_INFINITY]));
assert!(contains_nan(&[1.0, f64::NAN]));
assert!(!contains_nan(&[]));
}
#[test]
fn kahan_sum_matches_naive_sum_for_well_conditioned_data() {
let values: Vec<f64> = (1..=100).map(|i| i as f64).collect();
assert!((kahan_sum(&values) - 5050.0).abs() < 1e-9);
}
#[test]
fn kahan_sum_running_last_element_equals_kahan_sum() {
let values = vec![1.5, -2.5, 3.25, 7.0];
let running = kahan_sum_running(&values);
assert_eq!(running.len(), values.len());
assert!((*running.last().unwrap_or(&0.0) - kahan_sum(&values)).abs() < 1e-12);
}
#[test]
fn kahan_sum_of_empty_is_zero() {
assert_eq!(kahan_sum(&[]), 0.0);
assert!(kahan_sum_running(&[]).is_empty());
}
#[test]
fn kahan_sum_is_more_accurate_than_naive_summation_for_a_classic_ill_conditioned_case() {
let mut values = vec![1.0e16_f64];
values.extend(std::iter::repeat(1.0_f64).take(10_000));
values.push(-1.0e16_f64);
let naive: f64 = values.iter().sum();
let kahan = kahan_sum(&values);
assert!((kahan - 10_000.0).abs() < 1.0, "Kahan sum should recover the true total, got {kahan}");
assert!((kahan - 10_000.0).abs() < (naive - 10_000.0).abs(), "Kahan sum must be at least as accurate as naive summation here");
}
#[test]
fn mean_kahan_matches_hand_computed_average() {
assert!((mean_kahan(&[2.0, 4.0, 6.0, 8.0]) - 5.0).abs() < 1e-9);
}
#[test]
fn mean_kahan_of_empty_is_nan_not_a_panic() {
assert!(mean_kahan(&[]).is_nan());
}
#[test]
fn transform_error_display_is_human_readable() {
assert_eq!(TransformError::NonFinite { index: 3 }.to_string(), "non-finite value at index 3");
assert_eq!(TransformError::EmptyInput.to_string(), "no finite values to compute over");
}
}