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