use aatxe_core::stats::{
coefficient_of_variation, interquartile_range, mann_whitney_u, mean, median,
median_absolute_deviation, percentile, stddev, summarize_samples, trimmed_mean, welch_t,
};
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn mean_handles_empty_and_single() {
assert_eq!(mean(&[]), 0.0);
assert_eq!(mean(&[42.0]), 42.0);
assert!(close(mean(&[1.0, 2.0, 3.0, 4.0]), 2.5, 1e-12));
}
#[test]
fn median_is_outlier_robust() {
let xs = [1.0, 2.0, 3.0, 4.0, 1_000_000.0];
assert_eq!(median(&xs), 3.0);
assert!(mean(&xs) > 100.0);
}
#[test]
fn percentile_interpolates() {
let xs = [1.0, 2.0, 3.0, 4.0, 5.0];
assert!(close(percentile(&xs, 0.0), 1.0, 1e-12));
assert!(close(percentile(&xs, 50.0), 3.0, 1e-12));
assert!(close(percentile(&xs, 100.0), 5.0, 1e-12));
assert!(close(percentile(&xs, 12.5), 1.5, 1e-12));
}
#[test]
fn stddev_matches_known_value() {
let xs = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
assert!(close(stddev(&xs), (32.0_f64 / 7.0).sqrt(), 1e-9));
}
#[test]
fn cv_returns_zero_when_mean_is_zero() {
assert_eq!(coefficient_of_variation(&[]), 0.0);
assert_eq!(coefficient_of_variation(&[0.0, 0.0, 0.0]), 0.0);
}
#[test]
fn trimmed_mean_drops_extremes() {
let mut xs: Vec<f64> = (1..=20).map(|i| i as f64).collect();
xs[0] = -1_000.0;
xs[19] = 1_000.0;
let tm = trimmed_mean(&xs, 0.05);
assert!((tm - 10.5).abs() < 1.0, "trimmed mean was {}", tm);
}
#[test]
fn mad_handles_duplicates() {
assert_eq!(median_absolute_deviation(&[1.0; 5]), 0.0);
assert!(close(
median_absolute_deviation(&[1.0, 2.0, 3.0, 4.0, 5.0]),
1.0,
1e-9
));
}
#[test]
fn iqr_is_p75_minus_p25() {
let xs = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
let iqr = interquartile_range(&xs);
assert!(close(iqr, 4.0, 1e-9));
}
#[test]
fn summary_matches_piecewise_helpers() {
let xs: Vec<f64> = (1..=100).map(|i| i as f64).collect();
let s = summarize_samples(&xs);
assert!(close(s.mean, mean(&xs), 1e-9));
assert!(close(s.median, median(&xs), 1e-9));
assert!(close(s.stddev, stddev(&xs), 1e-9));
assert!(close(s.p95, percentile(&xs, 95.0), 1e-9));
assert!(close(s.iqr, interquartile_range(&xs), 1e-9));
assert!(close(s.mad, median_absolute_deviation(&xs), 1e-9));
}
#[test]
fn mw_u_detects_shift() {
let a: Vec<f64> = (0..30).map(|i| i as f64).collect();
let b: Vec<f64> = (100..130).map(|i| i as f64).collect();
let r = mann_whitney_u(&a, &b);
assert!(r.p < 1e-5, "expected very low p, got {}", r.p);
}
#[test]
fn mw_u_high_p_for_identical_distributions() {
let a: Vec<f64> = (0..30).map(|i| i as f64).collect();
let b: Vec<f64> = (0..30).map(|i| i as f64).collect();
let r = mann_whitney_u(&a, &b);
assert!(r.p > 0.5, "expected p ~1, got {}", r.p);
}
#[test]
fn mw_u_empty_inputs_return_one() {
let r = mann_whitney_u(&[], &[1.0, 2.0]);
assert_eq!(r.p, 1.0);
let r = mann_whitney_u(&[1.0, 2.0], &[]);
assert_eq!(r.p, 1.0);
}
#[test]
fn mw_u_handles_ties() {
let a = vec![5.0; 20];
let b = vec![5.0; 20];
let r = mann_whitney_u(&a, &b);
assert!(close(r.p, 1.0, 1e-9), "p was {}", r.p);
}
#[test]
fn welch_t_detects_shift() {
let a: Vec<f64> = (0..30).map(|i| i as f64).collect();
let b: Vec<f64> = (100..130).map(|i| i as f64).collect();
let r = welch_t(&a, &b);
assert!(r.p < 1e-5, "expected very low p, got {}", r.p);
}
#[test]
fn welch_t_tiny_inputs_safe() {
let r = welch_t(&[1.0], &[2.0, 3.0]);
assert_eq!(r.p, 1.0);
}
#[test]
fn summarize_single_sample() {
let s = summarize_samples(&[42.0]);
assert_eq!(s.mean, 42.0);
assert_eq!(s.median, 42.0);
assert_eq!(s.min, 42.0);
assert_eq!(s.max, 42.0);
assert_eq!(s.stddev, 0.0);
assert_eq!(s.cv, 0.0);
assert_eq!(s.iqr, 0.0);
assert_eq!(s.mad, 0.0);
}
#[test]
fn summarize_all_zero_samples() {
let s = summarize_samples(&[0.0; 50]);
assert_eq!(s.mean, 0.0);
assert_eq!(s.median, 0.0);
assert_eq!(s.cv, 0.0, "CV must coerce to 0 when mean=0, not NaN");
assert_eq!(s.mad, 0.0);
assert_eq!(s.iqr, 0.0);
assert_eq!(s.p95, 0.0);
}
#[test]
fn summarize_handles_constant_samples() {
let s = summarize_samples(&[100.0; 30]);
assert_eq!(s.mean, 100.0);
assert_eq!(s.median, 100.0);
assert_eq!(s.p99, 100.0);
assert_eq!(s.stddev, 0.0);
assert_eq!(s.cv, 0.0);
assert_eq!(s.iqr, 0.0);
assert_eq!(s.mad, 0.0);
}
#[test]
fn summarize_handles_negative_samples() {
let s = summarize_samples(&[-3.0, -1.0, 1.0, 3.0]);
assert_eq!(s.min, -3.0);
assert_eq!(s.max, 3.0);
assert_eq!(s.mean, 0.0);
assert_eq!(
s.cv, 0.0,
"mean=0 ⇒ CV must coerce to 0 even with non-zero stddev"
);
assert!(s.stddev > 0.0);
}
#[test]
fn mw_u_z_is_zero_when_distributions_overlap_perfectly() {
let a: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = a.clone();
let r = mann_whitney_u(&a, &b);
assert!(r.p > 0.5, "p should be high; got {}", r.p);
}
#[test]
fn mw_u_one_element_each_side_does_not_panic() {
let r = mann_whitney_u(&[1.0], &[2.0]);
assert!(r.p.is_finite());
assert!((0.0..=1.0).contains(&r.p));
}
#[test]
fn percentile_clamps_at_endpoints() {
let xs = [10.0, 20.0, 30.0];
assert_eq!(percentile(&xs, 0.0), 10.0);
assert_eq!(percentile(&xs, 100.0), 30.0);
assert_eq!(percentile(&xs, 50.0), 20.0);
}
#[test]
fn trimmed_mean_with_zero_trim_equals_mean() {
let xs: Vec<f64> = (1..=20).map(|i| i as f64).collect();
assert!(close(trimmed_mean(&xs, 0.0), mean(&xs), 1e-12));
}
#[test]
fn trimmed_mean_with_oversize_trim_falls_back_to_mean() {
let xs = vec![1.0, 2.0, 3.0];
let tm = trimmed_mean(&xs, 0.5);
assert!(tm.is_finite(), "got non-finite {tm}");
}
#[test]
fn iqr_zero_for_constant_samples() {
assert_eq!(interquartile_range(&[5.0; 20]), 0.0);
}
#[test]
fn cv_handles_single_element() {
assert_eq!(coefficient_of_variation(&[42.0]), 0.0);
}
#[test]
fn stddev_empty_is_zero() {
assert_eq!(stddev(&[]), 0.0);
}
#[test]
fn median_absolute_deviation_empty_is_zero() {
assert_eq!(median_absolute_deviation(&[]), 0.0);
}