1use std::cell::RefCell;
46use std::cmp::Ordering;
47use std::fmt;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum QuantileError {
52 InvalidN,
54 InvalidEpsilon,
56 InvalidRank,
58 EmptySummary,
60}
61
62impl fmt::Display for QuantileError {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 QuantileError::InvalidN => write!(f, "n must be greater than 0"),
66 QuantileError::InvalidEpsilon => write!(f, "epsilon must be positive and finite"),
67 QuantileError::InvalidRank => write!(f, "rank must be between 0.0 and 1.0"),
68 QuantileError::EmptySummary => write!(f, "cannot query an empty summary"),
69 }
70 }
71}
72
73impl std::error::Error for QuantileError {}
74
75#[derive(Debug, Clone)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77struct RankInfo<T>
78where
79 T: Clone,
80{
81 val: T,
82 rmin: i64,
83 rmax: i64,
84}
85
86impl<T> RankInfo<T>
87where
88 T: Clone,
89{
90 fn new(val: T, rmin: i64, rmax: i64) -> Self {
91 RankInfo { val, rmin, rmax }
92 }
93}
94
95impl<T: Clone + Ord> Ord for RankInfo<T> {
96 fn cmp(&self, other: &Self) -> Ordering {
97 self.val.cmp(&other.val)
98 }
99}
100
101impl<T: Clone + Ord> PartialOrd for RankInfo<T> {
102 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
103 Some(self.cmp(other))
104 }
105}
106
107impl<T: Clone + PartialEq> PartialEq for RankInfo<T> {
108 fn eq(&self, other: &Self) -> bool {
109 self.val == other.val
110 }
111}
112
113impl<T: Clone + PartialEq> Eq for RankInfo<T> {}
114
115#[derive(Clone)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct FixedSizeEpsilonSummary<T>
121where
122 T: Clone + Ord,
123{
124 epsilon: f64,
125 capacity: usize,
126 b: usize,
127 level: usize,
128 cnt: usize,
129 s: Vec<Vec<RankInfo<T>>>,
130 #[cfg_attr(feature = "serde", serde(skip))]
131 cached_s_m: RefCell<Option<Vec<RankInfo<T>>>>,
132}
133
134impl<T> FixedSizeEpsilonSummary<T>
135where
136 T: Clone + Ord,
137{
138 pub fn new(n: usize, epsilon: f64) -> Result<Self, QuantileError> {
139 if n == 0 {
140 return Err(QuantileError::InvalidN);
141 }
142
143 if !(epsilon > 0.0 && epsilon.is_finite()) {
144 return Err(QuantileError::InvalidEpsilon);
145 }
146
147 let epsilon_n: f64 = (n as f64) * epsilon;
149 let block_size = if epsilon_n > 1.0 {
150 ((epsilon_n.log2() / epsilon).floor() as usize).max(1)
151 } else {
152 n + 1
153 };
154 let number_of_levels = if epsilon_n > 1.0 {
158 let full_blocks = n / block_size;
159 (usize::BITS - full_blocks.leading_zeros()) as usize + 1
160 } else {
161 1
162 };
163
164 let mut s = vec![vec![]; number_of_levels];
165 s[0].reserve_exact(block_size);
166
167 Ok(FixedSizeEpsilonSummary {
168 epsilon,
169 capacity: n,
170 b: block_size,
171 level: number_of_levels,
172 cnt: 0,
173 s,
174 cached_s_m: RefCell::new(None),
175 })
176 }
177
178 pub fn update(&mut self, e: T) {
184 assert!(
185 self.cnt < self.capacity,
186 "FixedSizeEpsilonSummary capacity exceeded: constructed for n={} elements",
187 self.capacity
188 );
189 self.cached_s_m.get_mut().take();
190 let rank_info = RankInfo::new(e, 0, 0);
191 self.s[0].push(rank_info);
192
193 self.cnt += 1;
194 if self.s[0].len() < self.b {
195 return;
196 }
197
198 self.s[0].sort_unstable();
199 for (i, r) in self.s[0].iter_mut().enumerate() {
200 r.rmin = i as i64;
201 r.rmax = i as i64;
202 }
203
204 let compressed_size = self.b / 2;
205 let block = std::mem::replace(&mut self.s[0], Vec::with_capacity(self.b));
206 let mut s_c = compress(block, compressed_size, self.epsilon);
207 let mut stored = false;
208 for k in 1..self.level {
209 if self.s[k].is_empty() {
210 self.s[k] = s_c;
211 stored = true;
212 break;
213 } else {
214 let t = merge(s_c, &self.s[k]);
215 s_c = compress(t, compressed_size, self.epsilon);
216 self.s[k].clear();
217 }
218 }
219 debug_assert!(
220 stored,
221 "capacity invariant failed: capacity={}, count={}, block_size={}, levels={}",
222 self.capacity, self.cnt, self.b, self.level
223 );
224 }
225
226 pub fn query(&self, r: f64) -> Result<T, QuantileError> {
227 if !(0.0..=1.0).contains(&r) {
228 return Err(QuantileError::InvalidRank);
229 }
230 if self.cnt == 0 {
231 return Err(QuantileError::EmptySummary);
232 }
233
234 if self.cached_s_m.borrow().is_none() {
235 let mut s0_clone = self.s[0].clone();
237 s0_clone.sort_unstable();
238 for (i, r) in s0_clone.iter_mut().enumerate() {
239 r.rmin = i as i64;
240 r.rmax = i as i64;
241 }
242 let mut s_m = s0_clone;
243 for i in 1..self.level {
244 s_m = merge(s_m, &self.s[i]);
245 }
246 *self.cached_s_m.borrow_mut() = Some(s_m);
247 }
248
249 let rank: i64 = ((self.cnt as f64) * r).floor() as i64;
250 let epsilon_n: i64 = ((self.cnt as f64) * self.epsilon).floor() as i64;
251 let cache = self.cached_s_m.borrow();
252 let s_m = cache.as_ref().unwrap();
253 if let Some(e) = find_idx(s_m, rank, epsilon_n) {
254 Ok(e)
255 } else {
256 Ok(s_m.last().unwrap().val.clone())
257 }
258 }
259
260 fn calc_s_m(&self) -> Vec<RankInfo<T>> {
261 let mut s0_clone = self.s[0].clone();
262 s0_clone.sort_unstable();
263 for (i, r) in s0_clone.iter_mut().enumerate() {
264 r.rmin = i as i64;
265 r.rmax = i as i64;
266 }
267
268 let mut s_m = s0_clone;
269 for i in 1..self.level {
270 s_m = merge(s_m, &self.s[i])
271 }
272
273 compress(s_m, self.b, self.epsilon)
274 }
275
276 fn finalize(&mut self) {
277 let s_m = self.calc_s_m();
278 self.s.clear();
279 self.s.push(s_m);
280 }
281
282 #[inline]
283 #[must_use]
284 pub fn size(&self) -> usize {
285 self.cnt
286 }
287}
288
289impl<T: Clone + Ord + std::fmt::Debug> std::fmt::Debug for FixedSizeEpsilonSummary<T> {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("FixedSizeEpsilonSummary")
292 .field("epsilon", &self.epsilon)
293 .field("b", &self.b)
294 .field("level", &self.level)
295 .field("cnt", &self.cnt)
296 .finish()
297 }
298}
299
300fn merge<T: Clone + Ord>(s_a: Vec<RankInfo<T>>, s_b: &[RankInfo<T>]) -> Vec<RankInfo<T>> {
301 if s_a.is_empty() {
302 return s_b.to_vec();
303 }
304
305 if s_b.is_empty() {
306 return s_a;
307 }
308
309 let mut s_m = Vec::with_capacity(s_a.len() + s_b.len());
310
311 let mut i1 = 0;
312 let mut i2 = 0;
313 let mut from;
314
315 while i1 < s_a.len() || i2 < s_b.len() {
316 let val;
317 let rmin;
318 let rmax;
319
320 if i1 < s_a.len() && i2 < s_b.len() {
321 if s_a[i1].val < s_b[i2].val {
322 val = s_a[i1].val.clone();
323 from = 1;
324 } else {
325 val = s_b[i2].val.clone();
326 from = 2;
327 }
328 } else if i1 < s_a.len() && i2 >= s_b.len() {
329 val = s_a[i1].val.clone();
330 from = 1;
331 } else {
332 val = s_b[i2].val.clone();
333 from = 2;
334 }
335
336 if from == 1 {
337 if 0 < i2 && i2 < s_b.len() {
338 rmin = s_a[i1].rmin + s_b[i2 - 1].rmin;
339 rmax = s_a[i1].rmax + s_b[i2].rmax - 1;
340 } else if i2 == 0 {
341 rmin = s_a[i1].rmin;
342 rmax = s_a[i1].rmax + s_b[i2].rmax - 1;
343 } else {
344 rmin = s_a[i1].rmin + s_b[i2 - 1].rmin;
345 rmax = s_a[i1].rmax + s_b[i2 - 1].rmax;
346 }
347
348 i1 += 1;
349 } else {
350 if 0 < i1 && i1 < s_a.len() {
351 rmin = s_a[i1 - 1].rmin + s_b[i2].rmin;
352 rmax = s_a[i1].rmax + s_b[i2].rmax - 1;
353 } else if i1 == 0 {
354 rmin = s_b[i2].rmin;
355 rmax = s_a[i1].rmax + s_b[i2].rmax - 1;
356 } else {
357 rmin = s_a[i1 - 1].rmin + s_b[i2].rmin;
358 rmax = s_a[i1 - 1].rmax + s_b[i2].rmax;
359 }
360
361 i2 += 1;
362 }
363
364 let rank_info = RankInfo::new(val, rmin, rmax);
365 s_m.push(rank_info);
366 }
367
368 s_m
369}
370
371fn compress<T: Clone>(mut s0: Vec<RankInfo<T>>, block_size: usize, epsilon: f64) -> Vec<RankInfo<T>> {
372 let mut s0_range = 0;
373 let mut e: f64 = 0.0;
374
375 for r in &s0 {
376 if s0_range < r.rmax {
377 s0_range = r.rmax;
378 }
379
380 if (r.rmax - r.rmin) as f64 > e {
381 e = (r.rmax - r.rmin) as f64;
382 }
383 }
384
385 let epsilon_n: f64 = epsilon * (s0_range as f64);
386 assert!(2.0 * epsilon_n >= e, "precision condition violated.");
387
388 let mut i = 0;
389 let mut j = 0;
390 let mut k = 0;
391 let n = s0.len();
392 while i <= block_size && j < n {
393 let r = ((i as f64) * (s0_range as f64) / (block_size as f64)).floor() as i64;
394
395 while j < n {
396 if s0[j].rmax >= r {
397 break;
398 }
399
400 j += 1;
401 }
402
403 assert!(j < n, "unable to find the summary with precision given.");
404 s0[k] = s0[j].clone();
405 k += 1;
406 j += 1;
407 i += 1;
408 }
409
410 s0.truncate(k);
411 s0
412}
413
414#[inline]
415#[allow(clippy::many_single_char_names)]
416#[allow(clippy::comparison_chain)]
417fn is_boundary(x: usize, boundaries: &[usize; 32]) -> Option<usize> {
418 let mut l = 0;
419 let mut r = 31;
420
421 while l < r {
422 let m = l + (r - l) / 2;
423 if boundaries[m] < x {
424 l = m + 1;
425 } else {
426 r = m;
427 }
428 }
429
430 if l < 31 && boundaries[l] == x {
431 return Some(l);
432 }
433
434 None
435}
436
437fn find_idx<T: Clone + Ord>(s_m: &[RankInfo<T>], rank: i64, epsilon_n: i64) -> Option<T> {
438 if s_m.is_empty() {
439 return None;
440 }
441
442 let mut l = 0usize;
443 let mut r = s_m.len() - 1;
444 while l < r {
445 let m = l + (r - l) / 2;
446 if s_m[m].rmin < rank {
447 l = m + 1;
448 } else {
449 r = m;
450 }
451 }
452
453 while l < s_m.len() && s_m[l].rmin <= rank + epsilon_n {
457 if s_m[l].rmax <= rank + epsilon_n {
458 return Some(s_m[l].val.clone());
459 }
460 l += 1;
461 }
462
463 None
464}
465
466#[derive(Clone)]
467#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
468pub struct UnboundEpsilonSummary<T>
469where
470 T: Clone + Ord,
471{
472 epsilon: f64,
473 cnt: usize,
474 s: Vec<FixedSizeEpsilonSummary<T>>,
475 s_c: FixedSizeEpsilonSummary<T>,
476 boundaries: [usize; 32],
477 #[cfg_attr(feature = "serde", serde(skip))]
478 cached_s_m: RefCell<Option<Vec<RankInfo<T>>>>,
479}
480
481impl<T> UnboundEpsilonSummary<T>
482where
483 T: Clone + Ord,
484{
485 pub fn new(epsilon: f64) -> Result<Self, QuantileError> {
486 if !(epsilon > 0.0 && epsilon.is_finite()) {
487 return Err(QuantileError::InvalidEpsilon);
488 }
489
490 let s = vec![];
491
492 let n = (1.0_f64 / epsilon).floor() as usize;
493 let s_c = FixedSizeEpsilonSummary::new(n, epsilon / 2.0)?;
494 let mut boundaries: [usize; 32] = [0; 32];
495 for (i, b) in boundaries.iter_mut().enumerate() {
496 *b = (((usize::pow(2, i as u32) - 1) as f64) / epsilon).floor() as usize;
497 }
498
499 Ok(UnboundEpsilonSummary {
500 epsilon,
501 cnt: 0,
502 s,
503 s_c,
504 boundaries,
505 cached_s_m: RefCell::new(None),
506 })
507 }
508
509 pub fn update(&mut self, e: T) {
510 self.cached_s_m.get_mut().take();
511 self.s_c.update(e);
512
513 if let Some(x) = is_boundary(self.cnt + 1, &self.boundaries) {
514 if x + 1 >= self.boundaries.len() {
515 self.cnt += 1;
517 return;
518 }
519 self.s_c.finalize();
520
521 let upper_bound = self.boundaries[x + 1];
524 let n = upper_bound - self.cnt - 1;
525 let mut summary = FixedSizeEpsilonSummary::new(n, self.epsilon / 2.0).unwrap();
526 std::mem::swap(&mut self.s_c, &mut summary);
527
528 self.s.push(summary);
529 }
530 self.cnt += 1;
531 }
532
533 pub fn query(&self, r: f64) -> Result<T, QuantileError> {
534 if !(0.0..=1.0).contains(&r) {
535 return Err(QuantileError::InvalidRank);
536 }
537 if self.cnt == 0 {
538 return Err(QuantileError::EmptySummary);
539 }
540
541 if self.cached_s_m.borrow().is_none() {
542 let mut s_m = self.s_c.calc_s_m();
543 for i in 0..self.s.len() {
544 for j in 0..self.s[i].s.len() {
545 s_m = merge(s_m, &self.s[i].s[j])
546 }
547 }
548 *self.cached_s_m.borrow_mut() = Some(s_m);
549 }
550
551 let rank: i64 = ((self.cnt as f64) * r).floor() as i64;
552 let epsilon_n: i64 = ((self.cnt as f64) * self.epsilon).floor() as i64;
553 let cache = self.cached_s_m.borrow();
554 let s_m = cache.as_ref().unwrap();
555 if let Some(e) = find_idx(s_m, rank, epsilon_n) {
556 Ok(e)
557 } else {
558 Ok(s_m.last().unwrap().val.clone())
559 }
560 }
561
562 #[inline]
563 #[must_use]
564 pub fn size(&self) -> usize {
565 self.cnt
566 }
567}
568
569impl<T: Clone + Ord + std::fmt::Debug> std::fmt::Debug for UnboundEpsilonSummary<T> {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 f.debug_struct("UnboundEpsilonSummary")
572 .field("epsilon", &self.epsilon)
573 .field("cnt", &self.cnt)
574 .finish()
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use rand::rngs::StdRng;
582 use rand::Rng;
583 use rand::SeedableRng;
584 use rand_distr::Distribution;
585
586 #[test]
587 fn test_merge_and_compress() {
588 let mut s0 = Vec::with_capacity(4);
589 let mut s1 = Vec::with_capacity(4);
590
591 s0.push(RankInfo::new(2, 1, 1));
592 s0.push(RankInfo::new(4, 3, 4));
593 s0.push(RankInfo::new(8, 5, 6));
594 s0.push(RankInfo::new(17, 8, 8));
595
596 s1.push(RankInfo::new(1, 1, 1));
597 s1.push(RankInfo::new(7, 3, 3));
598 s1.push(RankInfo::new(12, 5, 6));
599 s1.push(RankInfo::new(15, 8, 8));
600
601 let merged = merge(s0, &s1);
602
603 assert_eq!(merged.len(), 8);
604 let merged_vals: Vec<i32> = merged.iter().map(|x| x.val).collect();
605 let merged_rmins: Vec<i64> = merged.iter().map(|x| x.rmin).collect();
606 let merged_rmaxs: Vec<i64> = merged.iter().map(|x| x.rmax).collect();
607 assert_eq!(merged_vals, vec![1, 2, 4, 7, 8, 12, 15, 17]);
608 assert_eq!(merged_rmins, vec![1, 2, 4, 6, 8, 10, 13, 16]);
609 assert_eq!(merged_rmaxs, vec![1, 3, 6, 8, 11, 13, 15, 16]);
610
611 let epsilon: f64 = 0.2;
612 let compressed = compress(merged, 4, epsilon);
613 let compressed_vals: Vec<i32> = compressed.iter().map(|x| x.val).collect();
614 assert_eq!(compressed_vals, vec![1, 4, 7, 12, 17]);
615 }
616
617 #[test]
618 fn test_fixedsize_constructor_returns_error_on_zero_n() {
619 assert!(matches!(
620 FixedSizeEpsilonSummary::<usize>::new(0, 0.1),
621 Err(QuantileError::InvalidN)
622 ));
623 }
624
625 #[test]
626 fn test_fixedsize_constructor_returns_error_on_negative_epsilon() {
627 assert!(matches!(
628 FixedSizeEpsilonSummary::<usize>::new(100, -0.1),
629 Err(QuantileError::InvalidEpsilon)
630 ));
631 }
632
633 #[test]
634 fn test_fixedsize_constructor_returns_error_on_nan_epsilon() {
635 assert!(matches!(
636 FixedSizeEpsilonSummary::<usize>::new(100, f64::NAN),
637 Err(QuantileError::InvalidEpsilon)
638 ));
639 }
640
641 #[test]
642 fn test_fixedsize_constructor_returns_error_on_inf_epsilon() {
643 assert!(matches!(
644 FixedSizeEpsilonSummary::<usize>::new(100, f64::INFINITY),
645 Err(QuantileError::InvalidEpsilon)
646 ));
647 }
648
649 #[test]
650 fn test_unbound_constructor_returns_error_on_negative_epsilon() {
651 assert!(matches!(
652 UnboundEpsilonSummary::<usize>::new(-0.1),
653 Err(QuantileError::InvalidEpsilon)
654 ));
655 }
656
657 #[test]
658 fn test_unbound_constructor_returns_error_on_nan_epsilon() {
659 assert!(matches!(
660 UnboundEpsilonSummary::<usize>::new(f64::NAN),
661 Err(QuantileError::InvalidEpsilon)
662 ));
663 }
664
665 #[test]
666 fn test_query_fixed_summary_with_insufficient_values() {
667 let epsilon = 0.1;
668 let n = 3;
669 let mut s = FixedSizeEpsilonSummary::new(n, epsilon).unwrap();
670 for i in 1..=n {
671 s.update(i);
672 }
673
674 let rank: f64 = 1.0;
675 let ans = s.query(rank).unwrap();
676 assert!(n == ans);
677 }
678
679 #[test]
680 fn test_query_with_small_n_on_fixedsize_summary() {
681 let epsilon = 0.1;
682 let n = 10;
683 let mut s = FixedSizeEpsilonSummary::new(n, epsilon).unwrap();
684 for i in 1..=n {
685 s.update(i);
686 }
687
688 for i in 1..=n {
689 let rank: f64 = ((i - 1) as f64) / (n as f64);
690 let ans = s.query(rank).unwrap();
691 assert!(i == ans);
692 }
693 }
694
695 #[test]
696 #[should_panic(expected = "FixedSizeEpsilonSummary capacity exceeded")]
697 fn test_fixedsize_update_panics_when_capacity_is_exceeded() {
698 let mut s = FixedSizeEpsilonSummary::new(10, 0.1).unwrap();
699 for i in 0..=10 {
700 s.update(i);
701 }
702 }
703
704 fn assert_rank_error<F>(n: usize, epsilon: f64, mut update: F, query: impl Fn(f64) -> u64)
705 where
706 F: FnMut(u64),
707 {
708 let mut rng = StdRng::seed_from_u64(42);
709 let mut records = Vec::with_capacity(n);
710 for _ in 0..n {
711 let value = rng.random::<u64>();
712 records.push(value);
713 update(value);
714 }
715 records.sort_unstable();
716
717 let allowed_error = (epsilon * n as f64).ceil() as usize;
718 for step in 0..=100 {
719 let rank = step as f64 / 100.0;
720 let target = (rank * n as f64).floor() as usize;
721 let value = query(rank);
722 let low = records.partition_point(|x| *x < value);
723 let high = records.partition_point(|x| *x <= value);
724
725 assert!(
726 low <= target.saturating_add(allowed_error) && target <= high.saturating_add(allowed_error),
727 "rank {rank}: value rank interval [{low}, {high}] misses target {target} by more than {allowed_error}"
728 );
729 }
730 }
731
732 #[test]
733 fn test_fixedsize_summary_respects_rank_error_contract() {
734 let n = 10_000;
735 let epsilon = 0.01;
736 let summary = RefCell::new(FixedSizeEpsilonSummary::new(n, epsilon).unwrap());
737 assert_rank_error(
738 n,
739 epsilon,
740 |value| summary.borrow_mut().update(value),
741 |rank| summary.borrow().query(rank).unwrap(),
742 );
743 }
744
745 #[test]
746 fn test_unbound_summary_respects_rank_error_contract() {
747 let n = 100_000;
748 let epsilon = 0.01;
749 let summary = RefCell::new(UnboundEpsilonSummary::new(epsilon).unwrap());
750 assert_rank_error(
751 n,
752 epsilon,
753 |value| summary.borrow_mut().update(value),
754 |rank| summary.borrow().query(rank).unwrap(),
755 );
756 }
757
758 #[test]
759 fn test_unbound_summary_respects_rank_error_contract_past_boundary_16() {
760 let n = 2_000_000;
761 let epsilon = 0.1;
762 let summary = RefCell::new(UnboundEpsilonSummary::new(epsilon).unwrap());
763 assert_rank_error(
764 n,
765 epsilon,
766 |value| summary.borrow_mut().update(value),
767 |rank| summary.borrow().query(rank).unwrap(),
768 );
769 }
770
771 #[test]
772 fn test_query_with_small_n_on_unbound_summary() {
773 let epsilon = 0.1;
774 let n = 10;
775 let mut s = UnboundEpsilonSummary::new(epsilon).unwrap();
776 for i in 1..=n {
777 s.update(i);
778 }
779
780 for i in 1..=n {
781 let rank: f64 = ((i - 1) as f64) / (n as f64);
782 let ans = s.query(rank).unwrap();
783 assert!(i == ans);
784 }
785 }
786
787 trait TestSummary<T> {
788 fn update(&mut self, value: T);
789 fn query(&self, rank: f64) -> T;
790 }
791
792 impl<T: Clone + Ord> TestSummary<T> for FixedSizeEpsilonSummary<T> {
793 fn update(&mut self, value: T) {
794 self.update(value);
795 }
796
797 fn query(&self, rank: f64) -> T {
798 self.query(rank).unwrap()
799 }
800 }
801
802 impl<T: Clone + Ord> TestSummary<T> for UnboundEpsilonSummary<T> {
803 fn update(&mut self, value: T) {
804 self.update(value);
805 }
806
807 fn query(&self, rank: f64) -> T {
808 self.query(rank).unwrap()
809 }
810 }
811
812 fn assert_distribution_queries<D, S>(mut summary: S, distribution: D, n: usize, tolerances: [f64; 4])
813 where
814 D: Distribution<f64>,
815 S: TestSummary<ordered_float::NotNan<f64>>,
816 {
817 let mut rng = StdRng::seed_from_u64(42);
818 let mut records = Vec::with_capacity(n);
819 for _ in 0..n {
820 records.push(ordered_float::NotNan::new(distribution.sample(&mut rng)).unwrap());
821 }
822 records.sort_unstable();
823 for &value in &records {
824 summary.update(value);
825 }
826
827 for (rank, index, tolerance) in [
828 (0.5, n / 2, tolerances[0]),
829 (0.0, 0, tolerances[1]),
830 (0.99, n * 99 / 100, tolerances[2]),
831 (1.0, n - 1, tolerances[3]),
832 ] {
833 assert!((summary.query(rank) - records[index]).abs() < tolerance);
834 }
835 }
836
837 #[test]
838 fn test_normal_distribution_generated_seq_on_fixed_summary() {
839 let n = 1_000_000;
840 assert_distribution_queries(
841 FixedSizeEpsilonSummary::new(n, 0.01).unwrap(),
842 rand_distr::Normal::new(0.5, 0.2).unwrap(),
843 n,
844 [0.01, 0.1, 0.01, 0.01],
845 );
846 }
847
848 #[test]
849 fn test_pareto_distribution_generated_seq_on_fixed_summary() {
850 let n = 1_000_000;
851 assert_distribution_queries(
852 FixedSizeEpsilonSummary::new(n, 0.001).unwrap(),
853 rand_distr::Pareto::new(5.0, 10.0).unwrap(),
854 n,
855 [0.01; 4],
856 );
857 }
858
859 #[test]
860 fn test_normal_distribution_generated_seq_on_unbound_summary() {
861 assert_distribution_queries(
862 UnboundEpsilonSummary::new(0.01).unwrap(),
863 rand_distr::Normal::new(0.5, 0.2).unwrap(),
864 1_000_000,
865 [0.01; 4],
866 );
867 }
868
869 #[test]
870 fn test_pareto_distribution_generated_seq_on_unbound_summary() {
871 assert_distribution_queries(
872 UnboundEpsilonSummary::new(0.001).unwrap(),
873 rand_distr::Pareto::new(5.0, 10.0).unwrap(),
874 1_000_000,
875 [0.01; 4],
876 );
877 }
878
879 #[test]
880 fn test_unbound_summary_clone_preserves_queries() {
881 let mut summary = UnboundEpsilonSummary::new(0.1).unwrap();
882 summary.update(1);
883 assert_eq!(summary.query(0.5), summary.clone().query(0.5));
884 }
885
886 #[test]
887 fn test_error_display_and_debug() {
888 let e = QuantileError::InvalidN;
889 assert_eq!(format!("{}", e), "n must be greater than 0");
890 let e = QuantileError::InvalidEpsilon;
891 assert_eq!(format!("{}", e), "epsilon must be positive and finite");
892 let e = QuantileError::InvalidRank;
893 assert_eq!(format!("{}", e), "rank must be between 0.0 and 1.0");
894 let e = QuantileError::EmptySummary;
895 assert_eq!(format!("{}", e), "cannot query an empty summary");
896
897 let e2 = e;
899 assert_eq!(e, e2);
900 }
901
902 #[test]
903 fn test_find_idx_empty_slice() {
904 let empty: Vec<RankInfo<i32>> = vec![];
905 assert!(find_idx(&empty, 0, 1).is_none());
906 }
907
908 #[test]
909 fn test_query_returns_error_on_empty_summary() {
910 let s = FixedSizeEpsilonSummary::<usize>::new(10, 0.1).unwrap();
911 assert!(matches!(s.query(0.5), Err(QuantileError::EmptySummary)));
912 }
913
914 #[test]
915 fn test_query_returns_error_on_invalid_rank() {
916 let mut s = FixedSizeEpsilonSummary::new(10, 0.1).unwrap();
917 s.update(1);
918 assert!(matches!(s.query(-0.1), Err(QuantileError::InvalidRank)));
919 assert!(matches!(s.query(1.1), Err(QuantileError::InvalidRank)));
920 assert!(matches!(s.query(f64::NAN), Err(QuantileError::InvalidRank)));
921 }
922
923 #[test]
924 fn test_query_is_immutable() {
925 let mut s = FixedSizeEpsilonSummary::new(10, 0.1).unwrap();
926 for i in 1..=10 {
927 s.update(i);
928 }
929 let s_ref = &s;
931 let _ = s_ref.query(0.5);
932 let _ = s_ref.query(0.9);
933 }
934
935 #[test]
936 fn test_debug_impls() {
937 let s = FixedSizeEpsilonSummary::<usize>::new(10, 0.1).unwrap();
938 let debug_str = format!("{:?}", s);
939 assert!(debug_str.contains("FixedSizeEpsilonSummary"));
940
941 let s = UnboundEpsilonSummary::<usize>::new(0.1).unwrap();
942 let debug_str = format!("{:?}", s);
943 assert!(debug_str.contains("UnboundEpsilonSummary"));
944 }
945
946 #[cfg(feature = "serde")]
947 #[test]
948 fn test_serde_roundtrip_fixedsize() {
949 let mut s = FixedSizeEpsilonSummary::new(100, 0.1).unwrap();
950 for i in 1..=100usize {
951 s.update(i);
952 }
953 let serialized = serde_json::to_string(&s).unwrap();
954 let deserialized: FixedSizeEpsilonSummary<usize> = serde_json::from_str(&serialized).unwrap();
955 assert_eq!(s.query(0.5).unwrap(), deserialized.query(0.5).unwrap());
956 }
957
958 #[cfg(feature = "serde")]
959 #[test]
960 fn test_serde_roundtrip_unbound() {
961 let mut s = UnboundEpsilonSummary::new(0.1).unwrap();
962 for i in 1..=100usize {
963 s.update(i);
964 }
965 let serialized = serde_json::to_string(&s).unwrap();
966 let deserialized: UnboundEpsilonSummary<usize> = serde_json::from_str(&serialized).unwrap();
967 assert_eq!(s.query(0.5).unwrap(), deserialized.query(0.5).unwrap());
968 }
969}