1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
/// A quantile that has both the raw value and a human-friendly display label.
///
/// We work with quantiles for optimal floating-point precison over percentiles, but most of the
/// time, monitoring systems show us percentiles, and usually in an abbreviated form: `p99`.
///
/// On top of holding the quantile value, we calculate the familiar "p99" style of label, doing the
/// appropriate percentile conversion.  Thus, if you have a quantile of `0.99`, the resulting label
/// is `p99`, and if you have a quantile of `0.999`, the resulting label is `p999`.
///
/// There are two special cases, where we label `0.0` and `1.0` as `min` and `max`, respectively.
#[derive(Debug, Clone, PartialEq)]
pub struct Quantile(f64, String);

impl Quantile {
    /// Creates a new [`Quantile`] from a floating-point value.
    ///
    /// All values are clamped between 0.0 and 1.0.
    pub fn new(quantile: f64) -> Quantile {
        let clamped = quantile.max(0.0);
        let clamped = clamped.min(1.0);
        let display = clamped * 100.0;

        let raw_label = format!("{}", clamped);
        let label = match raw_label.as_str() {
            "0" => "min".to_string(),
            "1" => "max".to_string(),
            _ => {
                let raw = format!("p{}", display);
                raw.replace(".", "")
            }
        };

        Quantile(clamped, label)
    }

    /// Gets the human-friendly display label.
    pub fn label(&self) -> &str {
        self.1.as_str()
    }

    /// Gets the raw quantile value.
    pub fn value(&self) -> f64 {
        self.0
    }
}

/// Parses a slice of floating-point values into a vector of [`Quantile`]s.
pub fn parse_quantiles(quantiles: &[f64]) -> Vec<Quantile> {
    quantiles.iter().map(|f| Quantile::new(*f)).collect()
}

#[cfg(test)]
mod tests {
    use super::{parse_quantiles, Quantile};

    #[test]
    fn test_quantiles() {
        let min = Quantile::new(0.0);
        assert_eq!(min.value(), 0.0);
        assert_eq!(min.label(), "min");

        let max = Quantile::new(1.0);
        assert_eq!(max.value(), 1.0);
        assert_eq!(max.label(), "max");

        let p99 = Quantile::new(0.99);
        assert_eq!(p99.value(), 0.99);
        assert_eq!(p99.label(), "p99");

        let p999 = Quantile::new(0.999);
        assert_eq!(p999.value(), 0.999);
        assert_eq!(p999.label(), "p999");

        let p9999 = Quantile::new(0.9999);
        assert_eq!(p9999.value(), 0.9999);
        assert_eq!(p9999.label(), "p9999");

        let under = Quantile::new(-1.0);
        assert_eq!(under.value(), 0.0);
        assert_eq!(under.label(), "min");

        let over = Quantile::new(1.2);
        assert_eq!(over.value(), 1.0);
        assert_eq!(over.label(), "max");
    }

    #[test]
    fn test_parse_quantiles() {
        let empty = vec![];
        let result = parse_quantiles(&empty);
        assert_eq!(result.len(), 0);

        let normal = vec![0.0, 0.5, 0.99, 0.999, 1.0];
        let result = parse_quantiles(&normal);
        assert_eq!(result.len(), 5);
        assert_eq!(result[0], Quantile::new(0.0));
        assert_eq!(result[1], Quantile::new(0.5));
        assert_eq!(result[2], Quantile::new(0.99));
        assert_eq!(result[3], Quantile::new(0.999));
        assert_eq!(result[4], Quantile::new(1.0));
    }
}