use crate::tests_stat::Alternative;
pub(super) fn kendall_two_sided_exact(
n: usize,
discordant: f64,
tau: f64,
alternative: Alternative,
) -> f64 {
let max_inv = n * (n - 1) / 2;
let counts = inversion_counts(n, max_inv);
let total: f64 = counts.iter().sum();
if total <= 0.0 {
return 1.0;
}
let d = discordant.round();
let d_idx = clamp_index(d, max_inv);
let lower: f64 = counts.iter().take(d_idx + 1).sum::<f64>() / total;
let upper: f64 = counts.iter().skip(d_idx).sum::<f64>() / total;
match alternative {
Alternative::Greater => lower.clamp(0.0, 1.0),
Alternative::Less => upper.clamp(0.0, 1.0),
Alternative::TwoSided => {
let tail = if tau >= 0.0 { lower } else { upper };
(2.0 * tail).clamp(0.0, 1.0)
}
}
}
fn clamp_index(d: f64, max_inv: usize) -> usize {
if d <= 0.0 {
return 0;
}
let mut idx = 0usize;
while idx < max_inv && f64_eq_or_less(idx + 1, d) {
idx += 1;
}
idx
}
fn f64_eq_or_less(count: usize, d: f64) -> bool {
let lo = u32::try_from(count).unwrap_or(u32::MAX);
f64::from(lo) <= d
}
fn inversion_counts(n: usize, max_inv: usize) -> Vec<f64> {
let mut counts = vec![0.0; max_inv + 1];
if let Some(first) = counts.first_mut() {
*first = 1.0;
}
let mut current_max = 0usize;
for i in 1..n {
let new_max = (current_max + i).min(max_inv);
let prefix = prefix_sums(&counts, new_max);
let mut next = vec![0.0; max_inv + 1];
for (k, slot) in next.iter_mut().enumerate().take(new_max + 1) {
let hi = prefix.get(k).copied().unwrap_or(0.0);
let lo = k
.checked_sub(i + 1)
.and_then(|idx| prefix.get(idx))
.copied()
.unwrap_or(0.0);
*slot = hi - lo;
}
counts = next;
current_max = new_max;
}
counts
}
fn prefix_sums(counts: &[f64], upto: usize) -> Vec<f64> {
let mut prefix = vec![0.0; upto + 1];
let mut running = 0.0;
for (k, slot) in prefix.iter_mut().enumerate() {
running += counts.get(k).copied().unwrap_or(0.0);
*slot = running;
}
prefix
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inversion_counts_for_four() {
let c = inversion_counts(4, 6);
assert_eq!(c, vec![1.0, 3.0, 5.0, 6.0, 5.0, 3.0, 1.0], "counts {c:?}");
}
#[test]
fn perfect_concordance_p_is_reciprocal_factorial() {
let p = kendall_two_sided_exact(4, 0.0, 1.0, Alternative::Greater);
assert!((p - 1.0 / 24.0).abs() < 1e-12, "p was {p}");
}
}