Skip to main content

perl_parser_core/syntax/
percentile.rs

1//! Percentile helpers for integer metric samples (previously `perl-percentile`).
2//!
3//! Computes nearest-rank percentiles for pre-sorted `u64` samples.
4
5/// Compute the nearest-rank percentile from a sorted sample slice.
6///
7/// The nearest-rank definition uses:
8///
9/// - `rank = ceil((pct / 100) * n)` where `n` is sample length.
10/// - returned value is sample at `rank - 1` (1-based to 0-based conversion).
11///
12/// # Parameters
13///
14/// - `sorted_values`: sample values sorted in ascending order.
15/// - `pct`: percentile in the range `0..=100`.
16///
17/// # Returns
18///
19/// - `0` when `sorted_values` is empty.
20/// - nearest-rank percentile value otherwise.
21#[must_use]
22pub fn nearest_rank_percentile(sorted_values: &[u64], pct: u64) -> u64 {
23    if sorted_values.is_empty() {
24        return 0;
25    }
26
27    let pct_clamped = pct.min(100);
28    let rank = ((pct_clamped as f64 / 100.0) * sorted_values.len() as f64).ceil() as usize;
29    sorted_values[rank.min(sorted_values.len()).saturating_sub(1)]
30}
31
32#[cfg(test)]
33mod tests {
34    use super::nearest_rank_percentile;
35
36    #[test]
37    fn nearest_rank_handles_empty_input() {
38        assert_eq!(nearest_rank_percentile(&[], 95), 0);
39    }
40
41    #[test]
42    fn nearest_rank_percentiles_match_expected_values() {
43        let sorted = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
44        assert_eq!(nearest_rank_percentile(&sorted, 50), 5);
45        assert_eq!(nearest_rank_percentile(&sorted, 95), 10);
46        assert_eq!(nearest_rank_percentile(&sorted, 99), 10);
47    }
48
49    #[test]
50    fn nearest_rank_clamps_high_percentiles() {
51        let sorted = [10, 20, 30];
52        assert_eq!(nearest_rank_percentile(&sorted, 1000), 30);
53    }
54
55    #[test]
56    fn nearest_rank_returns_first_value_for_zero_percentile() {
57        let sorted = [10, 20, 30];
58
59        assert_eq!(nearest_rank_percentile(&sorted, 0), 10);
60    }
61
62    #[test]
63    fn nearest_rank_rounds_up_fractional_ranks() {
64        let sorted = [5, 10, 15, 20];
65
66        assert_eq!(nearest_rank_percentile(&sorted, 26), 10);
67    }
68
69    #[test]
70    fn nearest_rank_handles_duplicate_sample_values() {
71        let sorted = [3, 3, 3, 7, 7, 9];
72
73        assert_eq!(nearest_rank_percentile(&sorted, 50), 3);
74        assert_eq!(nearest_rank_percentile(&sorted, 67), 7);
75    }
76
77    #[test]
78    fn nearest_rank_handles_single_value_samples() {
79        let sorted = [42];
80
81        assert_eq!(nearest_rank_percentile(&sorted, 0), 42);
82        assert_eq!(nearest_rank_percentile(&sorted, 50), 42);
83        assert_eq!(nearest_rank_percentile(&sorted, 100), 42);
84        assert_eq!(nearest_rank_percentile(&sorted, 150), 42);
85    }
86
87    #[test]
88    fn nearest_rank_uses_expected_values_at_percentile_boundaries() {
89        let sorted = [10, 20, 30, 40, 50];
90
91        assert_eq!(nearest_rank_percentile(&sorted, 1), 10);
92        assert_eq!(nearest_rank_percentile(&sorted, 20), 10);
93        assert_eq!(nearest_rank_percentile(&sorted, 21), 20);
94        assert_eq!(nearest_rank_percentile(&sorted, 80), 40);
95        assert_eq!(nearest_rank_percentile(&sorted, 81), 50);
96        assert_eq!(nearest_rank_percentile(&sorted, 100), 50);
97    }
98}