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///
7/// # Examples
8///
9/// ```
10/// use tokmd_math::round_f64;
11///
12/// assert_eq!(round_f64(12.34567, 2), 12.35);
13/// assert_eq!(round_f64(12.34567, 4), 12.3457);
14/// assert_eq!(round_f64(1.5, 0), 2.0);
15/// ```
16///
17/// Rounding at zero decimal places:
18///
19/// ```
20/// use tokmd_math::round_f64;
21///
22/// assert_eq!(round_f64(2.4, 0), 2.0);
23/// assert_eq!(round_f64(2.6, 0), 3.0);
24/// ```
25#[must_use]
26pub fn round_f64(value: f64, decimals: u32) -> f64 {
27 let factor = 10f64.powi(decimals as i32);
28 (value * factor).round() / factor
29}
30
31/// Return a 4-decimal ratio and guard division by zero.
32///
33/// # Examples
34///
35/// ```
36/// use tokmd_math::safe_ratio;
37///
38/// assert_eq!(safe_ratio(1, 4), 0.25);
39/// assert_eq!(safe_ratio(5, 0), 0.0); // division by zero returns 0
40/// ```
41///
42/// Fractional ratios are rounded to four decimal places:
43///
44/// ```
45/// use tokmd_math::safe_ratio;
46///
47/// assert_eq!(safe_ratio(1, 3), 0.3333);
48/// assert_eq!(safe_ratio(2, 3), 0.6667);
49/// ```
50#[must_use]
51pub fn safe_ratio(numer: usize, denom: usize) -> f64 {
52 if denom == 0 {
53 0.0
54 } else {
55 round_f64(numer as f64 / denom as f64, 4)
56 }
57}
58
59/// Return the `pct` percentile from an ascending-sorted integer slice.
60///
61/// # Examples
62///
63/// ```
64/// use tokmd_math::percentile;
65///
66/// let values = [10, 20, 30, 40, 50];
67/// assert_eq!(percentile(&values, 0.0), 10.0);
68/// assert_eq!(percentile(&values, 0.9), 50.0);
69/// assert_eq!(percentile(&[], 0.5), 0.0); // empty slice returns 0
70/// ```
71///
72/// Computing the median:
73///
74/// ```
75/// use tokmd_math::percentile;
76///
77/// let data = [1, 2, 3, 4, 5];
78/// assert_eq!(percentile(&data, 0.5), 3.0);
79/// ```
80#[must_use]
81pub fn percentile(sorted: &[usize], pct: f64) -> f64 {
82 if sorted.is_empty() {
83 return 0.0;
84 }
85 let idx = (pct * (sorted.len() as f64 - 1.0)).ceil() as usize;
86 sorted[idx.min(sorted.len() - 1)] as f64
87}
88
89/// Return the Gini coefficient for an ascending-sorted integer slice.
90///
91/// # Examples
92///
93/// ```
94/// use tokmd_math::gini_coefficient;
95///
96/// // Perfectly equal distribution has a Gini of 0
97/// assert!((gini_coefficient(&[5, 5, 5, 5]) - 0.0).abs() < 1e-10);
98///
99/// // Empty slice returns 0
100/// assert_eq!(gini_coefficient(&[]), 0.0);
101///
102/// // Unequal distribution produces a positive Gini
103/// assert!(gini_coefficient(&[1, 1, 1, 100]) > 0.0);
104/// ```
105///
106/// Single-element and all-zero slices:
107///
108/// ```
109/// use tokmd_math::gini_coefficient;
110///
111/// // A single element has zero inequality
112/// assert_eq!(gini_coefficient(&[42]), 0.0);
113///
114/// // All zeros also return 0 (no division by zero)
115/// assert_eq!(gini_coefficient(&[0, 0, 0]), 0.0);
116/// ```
117#[must_use]
118pub fn gini_coefficient(sorted: &[usize]) -> f64 {
119 if sorted.is_empty() {
120 return 0.0;
121 }
122 let n = sorted.len() as f64;
123 let sum: f64 = sorted.iter().map(|v| *v as f64).sum();
124 if sum == 0.0 {
125 return 0.0;
126 }
127 let mut accum = 0.0;
128 for (i, value) in sorted.iter().enumerate() {
129 let i = i as f64 + 1.0;
130 accum += (2.0 * i - n - 1.0) * (*value as f64);
131 }
132 accum / (n * sum)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn round_f64_rounds_expected_precision() {
141 // Avoid PI-like literals: Nix clippy denies clippy::approx_constant and
142 // lints test targets.
143 let value = 12.34567;
144 assert_eq!(round_f64(value, 2), 12.35);
145 assert_eq!(round_f64(value, 4), 12.3457);
146 }
147
148 #[test]
149 fn safe_ratio_guards_divide_by_zero() {
150 assert_eq!(safe_ratio(5, 0), 0.0);
151 assert_eq!(safe_ratio(1, 4), 0.25);
152 }
153
154 #[test]
155 fn percentile_returns_expected_values() {
156 let values = [10usize, 20, 30, 40, 50];
157 assert_eq!(percentile(&values, 0.0), 10.0);
158 assert_eq!(percentile(&values, 0.9), 50.0);
159 }
160
161 #[test]
162 fn gini_coefficient_handles_empty_and_uniform() {
163 assert_eq!(gini_coefficient(&[]), 0.0);
164 assert!((gini_coefficient(&[5, 5, 5, 5]) - 0.0).abs() < 1e-10);
165 }
166}