pub(crate) const MIN_VALUES: usize = 4;
pub(crate) fn sample_lmoments<'a>(
column: impl IntoIterator<Item = &'a f64>,
sorted: &mut Vec<f64>,
) -> Option<[f64; 3]> {
sorted.clear();
sorted.extend(column.into_iter().copied().filter(|x| !x.is_nan()));
if sorted.len() < MIN_VALUES {
return None;
}
sorted.sort_by(f64::total_cmp);
let mut sums = [0.0_f64; 3];
for (index, &value) in sorted.iter().enumerate() {
let rank = index as f64;
sums[0] += value;
let first_term = value * rank;
sums[1] += first_term;
sums[2] += first_term * (rank - 1.0);
}
let count = sorted.len() as f64;
let mut z = count;
sums[0] /= z;
let mut y = count - 1.0;
z *= y;
sums[1] /= z;
y -= 1.0;
z *= y;
sums[2] /= z;
let mut k = 3_usize;
let mut p0 = -1.0_f64;
for _ in 0..2 {
let ak = k as f64;
p0 = -p0;
let mut p = p0;
let mut temp = p * sums[0];
for (i, &sum) in sums.iter().enumerate().take(k).skip(1) {
let ai = i as f64;
p = -p * (ak + ai - 1.0) * (ak - ai) / (ai * ai);
temp += p * sum;
}
sums[k - 1] = temp;
k -= 1;
}
(sums[1] != 0.0).then(|| [sums[0], sums[1], sums[2] / sums[1]])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_a_hand_computed_sample() {
let [l1, l2, t3] = sample_lmoments(&[3.0, 1.0, 5.0, 2.0, 4.0], &mut Vec::new()).unwrap();
assert!((l1 - 3.0).abs() < 1e-15);
assert!((l2 - 1.0).abs() < 1e-15);
assert!(t3.abs() < 1e-15);
}
#[test]
fn missing_values_are_excluded_and_short_samples_have_none() {
let mut sorted = Vec::new();
let with_gaps = [f64::NAN, 2.0, 1.0, f64::NAN, 3.0, 4.0];
assert_eq!(
sample_lmoments(&with_gaps, &mut sorted),
sample_lmoments(&[1.0, 2.0, 3.0, 4.0], &mut sorted)
);
assert!(sample_lmoments(&[1.0, 2.0, 3.0, f64::NAN], &mut sorted).is_none());
}
#[test]
fn a_constant_sample_has_none() {
assert!(sample_lmoments(&[2.0; 6], &mut Vec::new()).is_none());
}
}