reifydb_value/value/percentile/
mod.rs1use serde::{Deserialize, Serialize};
5
6use crate::value::ordered_f64::OrderedF64;
7
8#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9pub struct Centroid {
10 mean: OrderedF64,
11 weight: OrderedF64,
12}
13
14impl Centroid {
15 pub fn new(mean: OrderedF64, weight: OrderedF64) -> Self {
16 Self {
17 mean,
18 weight,
19 }
20 }
21
22 pub fn mean(&self) -> OrderedF64 {
23 self.mean
24 }
25
26 pub fn weight(&self) -> OrderedF64 {
27 self.weight
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Percentiles {
33 centroids: Vec<Centroid>,
34 max_size: usize,
35 sum: OrderedF64,
36 count: OrderedF64,
37 min: Option<OrderedF64>,
38 max: Option<OrderedF64>,
39}
40
41impl Percentiles {
42 pub fn empty(max_size: usize) -> Self {
43 Self {
44 centroids: Vec::new(),
45 max_size,
46 sum: OrderedF64::zero(),
47 count: OrderedF64::zero(),
48 min: None,
49 max: None,
50 }
51 }
52
53 pub fn new(
54 centroids: Vec<Centroid>,
55 sum: OrderedF64,
56 count: OrderedF64,
57 min: Option<OrderedF64>,
58 max: Option<OrderedF64>,
59 max_size: usize,
60 ) -> Self {
61 Self {
62 centroids,
63 max_size,
64 sum,
65 count,
66 min,
67 max,
68 }
69 }
70
71 pub fn centroids(&self) -> &[Centroid] {
72 &self.centroids
73 }
74
75 pub fn max_size(&self) -> usize {
76 self.max_size
77 }
78
79 pub fn sum(&self) -> OrderedF64 {
80 self.sum
81 }
82
83 pub fn count(&self) -> OrderedF64 {
84 self.count
85 }
86
87 pub fn min(&self) -> Option<OrderedF64> {
88 self.min
89 }
90
91 pub fn max(&self) -> Option<OrderedF64> {
92 self.max
93 }
94
95 pub fn is_empty(&self) -> bool {
96 self.centroids.is_empty()
97 }
98
99 pub fn mean(&self) -> Option<OrderedF64> {
100 let count = self.count.value();
101 if count > 0.0 {
102 OrderedF64::try_from(self.sum.value() / count).ok()
103 } else {
104 None
105 }
106 }
107}
108
109impl Default for Percentiles {
110 fn default() -> Self {
111 Self::empty(DEFAULT_MAX_SIZE)
112 }
113}
114
115pub const DEFAULT_MAX_SIZE: usize = 100;
116
117#[cfg(test)]
118mod tests {
119 use postcard::{from_bytes, to_allocvec};
120
121 use crate::value::{
122 ordered_f64::OrderedF64,
123 percentile::{Centroid, Percentiles},
124 };
125
126 fn f(v: f64) -> OrderedF64 {
127 OrderedF64::try_from(v).expect("finite test constant")
128 }
129
130 fn sample() -> Percentiles {
131 Percentiles::new(
132 vec![Centroid::new(f(1.0), f(2.0)), Centroid::new(f(5.5), f(3.0))],
133 f(19.5),
134 f(5.0),
135 Some(f(1.0)),
136 Some(f(5.5)),
137 100,
138 )
139 }
140
141 #[test]
142 fn centroids_are_readable() {
143 let p = sample();
146 let centroids = p.centroids();
147
148 assert_eq!(centroids.len(), 2);
149 assert_eq!(centroids[0].mean(), f(1.0));
150 assert_eq!(centroids[0].weight(), f(2.0));
151 assert_eq!(centroids[1].mean(), f(5.5));
152 assert_eq!(centroids[1].weight(), f(3.0));
153 }
154
155 #[test]
156 fn round_trips_through_the_persisted_form() {
157 let p = sample();
159
160 let bytes = to_allocvec(&p).expect("encode");
161 let restored: Percentiles = from_bytes(&bytes).expect("decode");
162
163 assert_eq!(restored, p);
164 }
165
166 #[test]
167 fn an_empty_digest_reports_absent_bounds_not_a_sentinel() {
168 let p = Percentiles::empty(100);
172
173 assert!(p.is_empty());
174 assert_eq!(p.min(), None);
175 assert_eq!(p.max(), None);
176 assert_eq!(p.count(), OrderedF64::zero());
177 assert_eq!(p.sum(), OrderedF64::zero());
178 assert_eq!(p.mean(), None, "mean of no observations is absent, not NaN or zero");
179 }
180
181 #[test]
182 fn an_empty_digest_round_trips() {
183 let p = Percentiles::empty(64);
186
187 let bytes = to_allocvec(&p).expect("encode");
188 let restored: Percentiles = from_bytes(&bytes).expect("decode");
189
190 assert_eq!(restored, p);
191 assert_eq!(restored.max_size(), 64, "max_size must survive; it bounds later merges");
192 }
193
194 #[test]
195 fn mean_divides_sum_by_count() {
196 let p = sample();
197 assert_eq!(p.mean(), Some(f(19.5 / 5.0)));
198 }
199
200 #[test]
201 fn truncated_bytes_are_rejected() {
202 let p = sample();
204 let bytes = to_allocvec(&p).expect("encode");
205 let truncated = &bytes[..bytes.len() / 2];
206
207 assert!(from_bytes::<Percentiles>(truncated).is_err());
208 }
209}