use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PercentilesTracker {
q: [f64; 5],
n: [f64; 5],
n_prime: [f64; 5],
init_count: u8,
init_buffer: [f64; 5],
}
impl Default for PercentilesTracker {
fn default() -> Self {
Self::new()
}
}
impl PercentilesTracker {
pub fn new() -> Self {
Self {
q: [0.0; 5],
n: [1.0, 2.0, 3.0, 4.0, 5.0],
n_prime: [1.0, 2.0, 3.0, 4.0, 5.0],
init_count: 0,
init_buffer: [0.0; 5],
}
}
#[inline]
pub fn update(&mut self, value: f64) {
if !value.is_finite() {
return;
}
if self.init_count < 5 {
self.init_buffer[self.init_count as usize] = value;
self.init_count += 1;
if self.init_count == 5 {
self.init_buffer.sort_by(|a, b| a.partial_cmp(b).unwrap());
self.q = self.init_buffer;
}
return;
}
let k = if value < self.q[0] {
self.q[0] = value;
0
} else if value < self.q[1] {
0
} else if value < self.q[2] {
1
} else if value < self.q[3] {
2
} else if value < self.q[4] {
3
} else {
self.q[4] = value;
3
};
for i in (k + 1)..5 {
self.n[i] += 1.0;
}
let total = self.n[4];
self.n_prime[1] = 1.0 + 0.5 * total;
self.n_prime[2] = 1.0 + 0.95 * total;
self.n_prime[3] = 1.0 + 0.99 * total;
self.n_prime[4] = total;
for i in 1..4 {
let d = self.n_prime[i] - self.n[i];
if (d >= 1.0 && self.n[i + 1] - self.n[i] > 1.0)
|| (d <= -1.0 && self.n[i - 1] - self.n[i] < -1.0)
{
let d_sign = if d >= 0.0 { 1.0 } else { -1.0 };
let qi = self.q[i];
let qip1 = self.q[i + 1];
let qim1 = self.q[i - 1];
let ni = self.n[i];
let nip1 = self.n[i + 1];
let nim1 = self.n[i - 1];
let q_new = qi
+ d_sign / (nip1 - nim1)
* ((ni - nim1 + d_sign) * (qip1 - qi) / (nip1 - ni)
+ (nip1 - ni - d_sign) * (qi - qim1) / (ni - nim1));
if qim1 < q_new && q_new < qip1 {
self.q[i] = q_new;
} else {
let idx = if d_sign >= 0.0 { i + 1 } else { i - 1 };
self.q[i] = qi + d_sign * (self.q[idx] - qi) / (self.n[idx] - ni);
}
self.n[i] += d_sign;
}
}
}
#[inline]
pub fn get(&self) -> (f64, f64, f64) {
if self.init_count < 5 {
return (0.0, 0.0, 0.0);
}
(self.q[1], self.q[2], self.q[3])
}
#[inline]
pub fn min(&self) -> f64 {
if self.init_count < 5 {
return 0.0;
}
self.q[0]
}
#[inline]
pub fn max(&self) -> f64 {
if self.init_count < 5 {
return 0.0;
}
self.q[4]
}
#[inline]
pub fn is_initialized(&self) -> bool {
self.init_count >= 5
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Distribution {
count: u32,
mean: f64,
m2: f64,
percentiles: PercentilesTracker,
}
impl Default for Distribution {
fn default() -> Self {
Self::new()
}
}
impl Distribution {
#[inline]
pub fn new() -> Self {
Self {
count: 0,
mean: 0.0,
m2: 0.0,
percentiles: PercentilesTracker::new(),
}
}
#[inline]
pub fn update(&mut self, value: f64) {
if !value.is_finite() {
return;
}
self.count += 1;
let delta = value - self.mean;
self.mean += delta / self.count as f64;
let delta2 = value - self.mean;
self.m2 += delta * delta2;
self.percentiles.update(value);
}
#[inline]
pub fn count(&self) -> u32 {
self.count
}
#[inline]
pub fn mean(&self) -> f64 {
self.mean
}
#[inline]
pub fn stddev(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
(self.m2 / self.count as f64).sqrt()
}
#[inline]
pub fn variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.m2 / self.count as f64
}
#[inline]
pub fn z_score(&self, value: f64) -> f64 {
let std = self.stddev();
if std < 0.01 {
return 0.0;
}
(value - self.mean) / std
}
#[inline]
pub fn percentiles(&self) -> (f64, f64, f64) {
self.percentiles.get()
}
#[inline]
pub fn min(&self) -> f64 {
self.percentiles.min()
}
#[inline]
pub fn max(&self) -> f64 {
self.percentiles.max()
}
#[inline]
pub fn is_valid(&self) -> bool {
self.count >= 5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distribution_welford() {
let mut d = Distribution::new();
for v in [10.0, 20.0, 30.0, 40.0, 50.0] {
d.update(v);
}
assert!((d.mean() - 30.0).abs() < 0.01);
assert!((d.stddev() - 14.14).abs() < 0.5);
}
#[test]
fn test_distribution_z_score() {
let mut d = Distribution::new();
for v in [90.0, 95.0, 100.0, 105.0, 110.0] {
d.update(v);
}
assert!(d.z_score(100.0).abs() < 0.1);
let z = d.z_score(100.0 + 2.0 * d.stddev());
assert!((z - 2.0).abs() < 0.1);
}
#[test]
fn test_distribution_empty() {
let d = Distribution::new();
assert_eq!(d.count(), 0);
assert_eq!(d.mean(), 0.0);
assert_eq!(d.stddev(), 0.0);
assert_eq!(d.variance(), 0.0);
assert!(!d.is_valid());
}
#[test]
fn test_distribution_single_value() {
let mut d = Distribution::new();
d.update(42.0);
assert_eq!(d.count(), 1);
assert_eq!(d.mean(), 42.0);
assert_eq!(d.stddev(), 0.0); }
#[test]
fn test_distribution_variance() {
let mut d = Distribution::new();
for v in [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] {
d.update(v);
}
assert!((d.mean() - 5.0).abs() < 0.01);
assert!(d.variance() > 0.0);
}
#[test]
fn test_percentiles_tracker() {
let mut pt = PercentilesTracker::new();
for i in 1..=100 {
pt.update(i as f64);
}
let (p50, p95, p99) = pt.get();
assert!((p50 - 50.0).abs() < 5.0);
assert!((p95 - 95.0).abs() < 5.0);
assert!((p99 - 99.0).abs() < 3.0);
}
#[test]
fn test_percentiles_tracker_not_initialized() {
let mut pt = PercentilesTracker::new();
assert!(!pt.is_initialized());
pt.update(1.0);
pt.update(2.0);
assert!(!pt.is_initialized());
let (p50, p95, p99) = pt.get();
assert_eq!(p50, 0.0);
assert_eq!(p95, 0.0);
assert_eq!(p99, 0.0);
}
#[test]
fn test_percentiles_tracker_min_max() {
let mut pt = PercentilesTracker::new();
for i in 1..=10 {
pt.update(i as f64);
}
assert_eq!(pt.min(), 1.0);
assert_eq!(pt.max(), 10.0);
}
#[test]
fn test_distribution_z_score_edge_cases() {
let mut d = Distribution::new();
for _ in 0..10 {
d.update(50.0);
}
assert_eq!(d.z_score(100.0), 0.0);
}
#[test]
fn test_distribution_negative_values() {
let mut d = Distribution::new();
for v in [-10.0, -5.0, 0.0, 5.0, 10.0] {
d.update(v);
}
assert!((d.mean() - 0.0).abs() < 0.01);
assert!(d.stddev() > 0.0);
}
#[test]
fn test_distribution_large_values() {
let mut d = Distribution::new();
for v in [1e9, 1e9 + 1.0, 1e9 + 2.0, 1e9 + 3.0, 1e9 + 4.0] {
d.update(v);
}
assert!((d.mean() - (1e9 + 2.0)).abs() < 0.01);
}
#[test]
fn test_percentiles_with_outliers() {
let mut pt = PercentilesTracker::new();
for i in 1..=95 {
pt.update(i as f64);
}
for _ in 0..5 {
pt.update(1000.0);
}
let (p50, p95, p99) = pt.get();
assert!((p50 - 50.0).abs() < 10.0);
}
#[test]
fn test_distribution_nan_stability() {
let mut d = Distribution::new();
d.update(10.0);
d.update(f64::NAN);
d.update(f64::INFINITY);
d.update(20.0);
assert_eq!(d.count(), 2);
assert!((d.mean() - 15.0).abs() < 0.01);
assert!(d.stddev().is_finite());
}
#[test]
fn test_percentiles_nan_stability() {
let mut pt = PercentilesTracker::new();
for _ in 0..10 {
pt.update(10.0);
pt.update(f64::NAN);
}
let (p50, p95, p99) = pt.get();
assert!(p50.is_finite());
assert!(p95.is_finite());
assert!(p99.is_finite());
}
}