Skip to main content

tokmd_math/
lib.rs

1//! Deterministic numeric and statistical helpers.
2
3#![forbid(unsafe_code)]
4
5/// Round a floating point value to `decimals` decimal places.
6#[must_use]
7pub fn round_f64(value: f64, decimals: u32) -> f64 {
8    let factor = 10f64.powi(decimals as i32);
9    (value * factor).round() / factor
10}
11
12/// Return a 4-decimal ratio and guard division by zero.
13#[must_use]
14pub fn safe_ratio(numer: usize, denom: usize) -> f64 {
15    if denom == 0 {
16        0.0
17    } else {
18        round_f64(numer as f64 / denom as f64, 4)
19    }
20}
21
22/// Return the `pct` percentile from an ascending-sorted integer slice.
23#[must_use]
24pub fn percentile(sorted: &[usize], pct: f64) -> f64 {
25    if sorted.is_empty() {
26        return 0.0;
27    }
28    let idx = (pct * (sorted.len() as f64 - 1.0)).ceil() as usize;
29    sorted[idx.min(sorted.len() - 1)] as f64
30}
31
32/// Return the Gini coefficient for an ascending-sorted integer slice.
33#[must_use]
34pub fn gini_coefficient(sorted: &[usize]) -> f64 {
35    if sorted.is_empty() {
36        return 0.0;
37    }
38    let n = sorted.len() as f64;
39    let sum: f64 = sorted.iter().map(|v| *v as f64).sum();
40    if sum == 0.0 {
41        return 0.0;
42    }
43    let mut accum = 0.0;
44    for (i, value) in sorted.iter().enumerate() {
45        let i = i as f64 + 1.0;
46        accum += (2.0 * i - n - 1.0) * (*value as f64);
47    }
48    accum / (n * sum)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn round_f64_rounds_expected_precision() {
57        // Avoid PI-like literals: Nix clippy denies clippy::approx_constant and
58        // lints test targets.
59        let value = 12.34567;
60        assert_eq!(round_f64(value, 2), 12.35);
61        assert_eq!(round_f64(value, 4), 12.3457);
62    }
63
64    #[test]
65    fn safe_ratio_guards_divide_by_zero() {
66        assert_eq!(safe_ratio(5, 0), 0.0);
67        assert_eq!(safe_ratio(1, 4), 0.25);
68    }
69
70    #[test]
71    fn percentile_returns_expected_values() {
72        let values = [10usize, 20, 30, 40, 50];
73        assert_eq!(percentile(&values, 0.0), 10.0);
74        assert_eq!(percentile(&values, 0.9), 50.0);
75    }
76
77    #[test]
78    fn gini_coefficient_handles_empty_and_uniform() {
79        assert_eq!(gini_coefficient(&[]), 0.0);
80        assert!((gini_coefficient(&[5, 5, 5, 5]) - 0.0).abs() < 1e-10);
81    }
82}