pub(super) fn mid_ranks(values: &[f64]) -> Vec<f64> {
let n = values.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&i, &j| {
let a = values.get(i).copied().unwrap_or(f64::NAN);
let b = values.get(j).copied().unwrap_or(f64::NAN);
a.total_cmp(&b)
});
let mut ranks = vec![0.0; n];
let mut i = 0;
while i < n {
let mut j = i + 1;
let vi = order.get(i).and_then(|&k| values.get(k)).copied();
while j < n {
let vj = order.get(j).and_then(|&k| values.get(k)).copied();
if vi
.zip(vj)
.is_some_and(|(a, b)| (a - b).abs() < f64::EPSILON)
{
j += 1;
} else {
break;
}
}
let first = u32::try_from(i + 1).unwrap_or(u32::MAX);
let last = u32::try_from(j).unwrap_or(u32::MAX);
let avg = f64::midpoint(f64::from(first), f64::from(last));
for slot in order.get(i..j).into_iter().flatten() {
if let Some(r) = ranks.get_mut(*slot) {
*r = avg;
}
}
i = j;
}
ranks
}
pub(super) fn tie_correction(values: &[f64]) -> f64 {
let mut sorted = values.to_vec();
sorted.sort_by(f64::total_cmp);
let mut total = 0.0;
let mut i = 0;
let n = sorted.len();
while i < n {
let mut j = i + 1;
while j < n
&& sorted
.get(i)
.zip(sorted.get(j))
.is_some_and(|(a, b)| (a - b).abs() < f64::EPSILON)
{
j += 1;
}
let count = u32::try_from(j - i).unwrap_or(u32::MAX);
let t = f64::from(count);
total += t.mul_add(t * t, -t);
i = j;
}
total
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn distinct_values_rank_sequentially() {
let r = mid_ranks(&[30.0, 10.0, 20.0]);
assert_eq!(r, vec![3.0, 1.0, 2.0], "ranks were {r:?}");
}
#[test]
fn ties_share_average_rank() {
let r = mid_ranks(&[10.0, 20.0, 20.0, 40.0]);
assert_eq!(r, vec![1.0, 2.5, 2.5, 4.0], "ranks were {r:?}");
}
#[test]
fn tie_correction_counts_groups() {
let c = tie_correction(&[10.0, 20.0, 20.0, 40.0]);
assert!((c - 6.0).abs() < 1e-12, "correction was {c}");
}
}