Skip to main content

zw_fast_quantile/
lib.rs

1//! Zhang Wang Fast Approximate Quantiles Algorithm in Rust
2//!
3//! ## Installation
4//!
5//! Add this to your `Cargo.toml`:
6//!
7//! ```toml
8//! [dependencies]
9//! zw-fast-quantile = "1.0"
10//! ```
11//!
12//! ## Example
13//!
14//! ```rust
15//! use zw_fast_quantile::FixedSizeEpsilonSummary;
16//!
17//! let epsilon = 0.1;
18//! let n = 10;
19//! let mut s = FixedSizeEpsilonSummary::new(n, epsilon).unwrap();
20//! for i in 1..=n {
21//!     s.update(i);
22//! }
23//!
24//! let ans = s.query(0.0).unwrap();
25//! let expected = 1;
26//! assert!(expected == ans);
27//! ```
28//!
29//!
30//! ```rust
31//! use zw_fast_quantile::UnboundEpsilonSummary;
32//!
33//! let epsilon = 0.1;
34//! let n = 10;
35//! let mut s = UnboundEpsilonSummary::new(epsilon).unwrap();
36//! for i in 1..=n {
37//!     s.update(i);
38//! }
39//!
40//! let ans = s.query(0.0).unwrap();
41//! let expected = 1;
42//! assert!(expected == ans);
43//! ```
44//!
45use std::cell::RefCell;
46use std::cmp::Ordering;
47use std::fmt;
48
49/// Errors that can occur when constructing or querying a quantile summary.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum QuantileError {
52    /// `n` must be greater than 0.
53    InvalidN,
54    /// `epsilon` must be positive and finite.
55    InvalidEpsilon,
56    /// Rank must be between 0.0 and 1.0 (inclusive), and not NaN.
57    InvalidRank,
58    /// Cannot query an empty summary (no values have been inserted).
59    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))]
117/// An epsilon-approximate quantile summary for a stream with a known size.
118///
119/// The summary accepts at most the `n` elements declared in [`Self::new`].
120pub 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        // block_size = floor(log2(epsilon * N) / epsilon)
148        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        // Full blocks propagate through the merge levels like binary carries.
155        // The bit width of their maximum count gives the required merge levels;
156        // add one for level 0.
157        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    /// Adds an element to the summary.
179    ///
180    /// # Panics
181    ///
182    /// Panics if more than the `n` elements declared in [`Self::new`] are added.
183    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            // Clone s[0] and sort — do NOT mutate self.s
236            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    // Scan forward within the epsilon window.
454    // The compress invariant guarantees that if a valid element exists,
455    // it is at or after the binary search landing point.
456    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                // The pre-computed boundary table is exhausted.
516                self.cnt += 1;
517                return;
518            }
519            self.s_c.finalize();
520
521            // Zhang and Wang, SSDBM 2007, section 3.2 defines P_x as the
522            // interval between boundaries x and x + 1.
523            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        // Verify Copy + Clone + PartialEq + Eq
898        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        // query takes &self, not &mut self
930        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}