pub(crate) const MAD_TO_SIGMA: f64 = 1.482_602_218_505_602;
pub(crate) fn median_in_place(values: &mut [f64]) -> Option<f64> {
let length = values.len();
if length == 0 {
return None;
}
let mid = length / 2;
let compare = |a: &f64, b: &f64| a.partial_cmp(b).expect("non-finite value in median input");
let (left, pivot, _) = values.select_nth_unstable_by(mid, compare);
if length % 2 == 1 {
Some(*pivot)
} else {
let lower = left.iter().copied().reduce(f64::max).unwrap();
Some(0.5 * (lower + *pivot))
}
}
pub(crate) fn mad_sigma(samples: &[f64]) -> f64 {
if samples.is_empty() {
return f64::NAN;
}
let mut scratch = samples.to_vec();
let center = median_in_place(&mut scratch).unwrap();
for value in scratch.iter_mut() {
*value = (*value - center).abs();
}
let mad = median_in_place(&mut scratch).unwrap();
MAD_TO_SIGMA * mad
}
#[cfg(test)]
mod tests {
use super::*;
fn naive_median(values: &mut [f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let length = values.len();
Some(if length % 2 == 1 {
values[length / 2]
} else {
0.5 * (values[length / 2 - 1] + values[length / 2])
})
}
#[test]
fn empty_slice_returns_none() {
let mut values: Vec<f64> = Vec::new();
assert_eq!(median_in_place(&mut values), None);
}
#[test]
fn single_element_returns_itself() {
let mut values = vec![3.5];
assert_eq!(median_in_place(&mut values), Some(3.5));
}
#[test]
fn two_elements_returns_average() {
let mut values = vec![1.0, 4.0];
assert_eq!(median_in_place(&mut values), Some(2.5));
}
#[test]
fn odd_length_returns_middle() {
let mut values = vec![3.0, 1.0, 5.0, 2.0, 4.0];
assert_eq!(median_in_place(&mut values), Some(3.0));
}
#[test]
fn even_length_returns_average_of_two_middles() {
let mut values = vec![3.0, 1.0, 4.0, 2.0];
assert_eq!(median_in_place(&mut values), Some(2.5));
}
#[test]
fn duplicates_around_pivot() {
let mut values = vec![1.0, 5.0, 5.0, 5.0, 5.0, 5.0];
assert_eq!(median_in_place(&mut values), Some(5.0));
}
#[test]
fn mad_sigma_empty_returns_nan() {
assert!(mad_sigma(&[]).is_nan());
}
#[test]
fn mad_sigma_odd_length_matches_hand_calculation() {
let got = mad_sigma(&[1.0, 2.0, 3.0, 4.0, 5.0]);
assert!((got - MAD_TO_SIGMA).abs() < 1e-15);
}
#[test]
fn mad_sigma_even_length_matches_hand_calculation() {
let got = mad_sigma(&[1.0, 2.0, 3.0, 4.0]);
assert!((got - MAD_TO_SIGMA).abs() < 1e-15);
}
#[test]
fn mad_sigma_invariant_under_permutation() {
let baseline = mad_sigma(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
let permuted = mad_sigma(&[7.0, 1.0, 5.0, 3.0, 6.0, 2.0, 4.0]);
assert_eq!(baseline, permuted);
}
#[test]
fn mad_sigma_scales_linearly() {
let baseline = mad_sigma(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let scaled = mad_sigma(&[3.0, 6.0, 9.0, 12.0, 15.0]);
assert!((scaled - 3.0 * baseline).abs() < 1e-14);
}
#[test]
fn matches_naive_reference_across_random_inputs() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = || {
state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
};
for trial in 1..=200usize {
let length = (trial % 23) + 1; let values: Vec<f64> = (0..length)
.map(|_| {
let signed = next() as i64 as f64;
signed / (i64::MAX as f64 / 100.0)
})
.collect();
let mut input = values.clone();
let mut reference_input = values.clone();
let got = median_in_place(&mut input);
let want = naive_median(&mut reference_input);
assert_eq!(got, want, "trial {trial} values {values:?}");
}
}
}