metrics_util/
quantile.rs

1/// A quantile that has both the raw value and a human-friendly display label.
2///
3/// We work with quantiles for optimal floating-point precision over percentiles, but most of the
4/// time, monitoring systems show us percentiles, and usually in an abbreviated form: `p99`.
5///
6/// On top of holding the quantile value, we calculate the familiar "p99" style of label, doing the
7/// appropriate percentile conversion.  Thus, if you have a quantile of `0.99`, the resulting label
8/// is `p99`, and if you have a quantile of `0.999`, the resulting label is `p999`.
9///
10/// There are two special cases, where we label `0.0` and `1.0` as `min` and `max`, respectively.
11#[derive(Debug, Clone, PartialEq)]
12pub struct Quantile(f64, String);
13
14impl Quantile {
15    /// Creates a new [`Quantile`] from a floating-point value.
16    ///
17    /// All values are clamped between 0.0 and 1.0.
18    pub fn new(quantile: f64) -> Quantile {
19        let clamped = quantile.max(0.0);
20        let clamped = clamped.min(1.0);
21        let display = clamped * 100.0;
22
23        let raw_label = format!("{}", clamped);
24        let label = match raw_label.as_str() {
25            "0" => "min".to_string(),
26            "1" => "max".to_string(),
27            _ => {
28                let raw = format!("p{}", display);
29                raw.replace('.', "")
30            }
31        };
32
33        Quantile(clamped, label)
34    }
35
36    /// Gets the human-friendly display label.
37    pub fn label(&self) -> &str {
38        self.1.as_str()
39    }
40
41    /// Gets the raw quantile value.
42    pub fn value(&self) -> f64 {
43        self.0
44    }
45}
46
47/// Parses a slice of floating-point values into a vector of [`Quantile`]s.
48pub fn parse_quantiles(quantiles: &[f64]) -> Vec<Quantile> {
49    quantiles.iter().map(|f| Quantile::new(*f)).collect()
50}
51
52#[cfg(test)]
53mod tests {
54    use super::{parse_quantiles, Quantile};
55
56    #[test]
57    fn test_quantiles() {
58        let min = Quantile::new(0.0);
59        assert_eq!(min.value(), 0.0);
60        assert_eq!(min.label(), "min");
61
62        let max = Quantile::new(1.0);
63        assert_eq!(max.value(), 1.0);
64        assert_eq!(max.label(), "max");
65
66        let p99 = Quantile::new(0.99);
67        assert_eq!(p99.value(), 0.99);
68        assert_eq!(p99.label(), "p99");
69
70        let p999 = Quantile::new(0.999);
71        assert_eq!(p999.value(), 0.999);
72        assert_eq!(p999.label(), "p999");
73
74        let p9999 = Quantile::new(0.9999);
75        assert_eq!(p9999.value(), 0.9999);
76        assert_eq!(p9999.label(), "p9999");
77
78        let under = Quantile::new(-1.0);
79        assert_eq!(under.value(), 0.0);
80        assert_eq!(under.label(), "min");
81
82        let over = Quantile::new(1.2);
83        assert_eq!(over.value(), 1.0);
84        assert_eq!(over.label(), "max");
85    }
86
87    #[test]
88    fn test_parse_quantiles() {
89        let empty = vec![];
90        let result = parse_quantiles(&empty);
91        assert_eq!(result.len(), 0);
92
93        let normal = vec![0.0, 0.5, 0.99, 0.999, 1.0];
94        let result = parse_quantiles(&normal);
95        assert_eq!(result.len(), 5);
96        assert_eq!(result[0], Quantile::new(0.0));
97        assert_eq!(result[1], Quantile::new(0.5));
98        assert_eq!(result[2], Quantile::new(0.99));
99        assert_eq!(result[3], Quantile::new(0.999));
100        assert_eq!(result[4], Quantile::new(1.0));
101    }
102}