splinter_rs/
splinter.rs

1use std::{fmt::Debug, ops::Deref};
2
3use bytes::Bytes;
4
5use crate::{
6    Cut, Encodable, Merge, Optimizable, SplinterRef,
7    codec::{encoder::Encoder, footer::Footer},
8    cow::CowSplinter,
9    level::High,
10    partition::Partition,
11    traits::{PartitionRead, PartitionWrite},
12};
13
14/// A compressed bitmap optimized for small, sparse sets of 32-bit unsigned integers.
15///
16/// `Splinter` is the main owned data structure that can be built incrementally by inserting
17/// values and then optimized for size and query performance. It uses a 256-way tree structure
18/// by decomposing integers into big-endian component bytes, with nodes optimized into four
19/// different storage classes: tree, vec, bitmap, and run.
20///
21/// For zero-copy querying of serialized data, see [`SplinterRef`].
22/// For a clone-on-write wrapper, see [`CowSplinter`].
23///
24/// # Examples
25///
26/// Basic usage:
27///
28/// ```
29/// use splinter_rs::{Splinter, PartitionWrite, PartitionRead, Optimizable};
30///
31/// let mut splinter = Splinter::EMPTY;
32///
33/// // Insert some values
34/// splinter.insert(1024);
35/// splinter.insert(2048);
36/// splinter.insert(123);
37///
38/// // Check membership
39/// assert!(splinter.contains(1024));
40/// assert!(!splinter.contains(999));
41///
42/// // Get cardinality
43/// assert_eq!(splinter.cardinality(), 3);
44///
45/// // Optimize for better compression, recommended before encoding to bytes.
46/// splinter.optimize();
47/// ```
48///
49/// Building from iterator:
50///
51/// ```
52/// use splinter_rs::{Splinter, PartitionRead};
53///
54/// let values = vec![100, 200, 300, 400];
55/// let splinter: Splinter = values.into_iter().collect();
56///
57/// assert_eq!(splinter.cardinality(), 4);
58/// assert!(splinter.contains(200));
59/// ```
60#[derive(Clone, PartialEq, Eq, Default, Debug)]
61pub struct Splinter(Partition<High>);
62
63static_assertions::const_assert_eq!(std::mem::size_of::<Splinter>(), 40);
64
65impl Splinter {
66    /// An empty Splinter, suitable for usage in a const context.
67    pub const EMPTY: Self = Splinter(Partition::EMPTY);
68
69    /// Encodes this splinter into a [`SplinterRef`] for zero-copy querying.
70    ///
71    /// This method serializes the splinter data and returns a [`SplinterRef<Bytes>`]
72    /// that can be used for efficient read-only operations without deserializing
73    /// the underlying data structure.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use splinter_rs::{Splinter, PartitionWrite, PartitionRead};
79    ///
80    /// let mut splinter = Splinter::EMPTY;
81    /// splinter.insert(42);
82    /// splinter.insert(1337);
83    ///
84    /// let splinter_ref = splinter.encode_to_splinter_ref();
85    /// assert_eq!(splinter_ref.cardinality(), 2);
86    /// assert!(splinter_ref.contains(42));
87    /// ```
88    pub fn encode_to_splinter_ref(&self) -> SplinterRef<Bytes> {
89        SplinterRef { data: self.encode_to_bytes() }
90    }
91}
92
93impl FromIterator<u32> for Splinter {
94    fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self {
95        Self(Partition::<High>::from_iter(iter))
96    }
97}
98
99impl PartitionRead<High> for Splinter {
100    /// Returns the total number of elements in this splinter.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
106    ///
107    /// let mut splinter = Splinter::EMPTY;
108    /// assert_eq!(splinter.cardinality(), 0);
109    ///
110    /// splinter.insert(100);
111    /// splinter.insert(200);
112    /// splinter.insert(300);
113    /// assert_eq!(splinter.cardinality(), 3);
114    /// ```
115    #[inline]
116    fn cardinality(&self) -> usize {
117        self.0.cardinality()
118    }
119
120    /// Returns `true` if this splinter contains no elements.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
126    ///
127    /// let mut splinter = Splinter::EMPTY;
128    /// assert!(splinter.is_empty());
129    ///
130    /// splinter.insert(42);
131    /// assert!(!splinter.is_empty());
132    /// ```
133    #[inline]
134    fn is_empty(&self) -> bool {
135        self.0.is_empty()
136    }
137
138    /// Returns `true` if this splinter contains the specified value.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
144    ///
145    /// let mut splinter = Splinter::EMPTY;
146    /// splinter.insert(42);
147    /// splinter.insert(1337);
148    ///
149    /// assert!(splinter.contains(42));
150    /// assert!(splinter.contains(1337));
151    /// assert!(!splinter.contains(999));
152    /// ```
153    #[inline]
154    fn contains(&self, value: u32) -> bool {
155        self.0.contains(value)
156    }
157
158    /// Returns the number of elements in this splinter that are less than or equal to the given value.
159    ///
160    /// This is also known as the "rank" of the value in the sorted sequence of all elements.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
166    ///
167    /// let mut splinter = Splinter::EMPTY;
168    /// splinter.insert(10);
169    /// splinter.insert(20);
170    /// splinter.insert(30);
171    ///
172    /// assert_eq!(splinter.rank(5), 0);   // No elements <= 5
173    /// assert_eq!(splinter.rank(10), 1);  // One element <= 10
174    /// assert_eq!(splinter.rank(25), 2);  // Two elements <= 25
175    /// assert_eq!(splinter.rank(30), 3);  // Three elements <= 30
176    /// assert_eq!(splinter.rank(50), 3);  // Three elements <= 50
177    /// ```
178    #[inline]
179    fn rank(&self, value: u32) -> usize {
180        self.0.rank(value)
181    }
182
183    /// Returns the element at the given index in the sorted sequence, or `None` if the index is out of bounds.
184    ///
185    /// The index is 0-based, so `select(0)` returns the smallest element.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
191    ///
192    /// let mut splinter = Splinter::EMPTY;
193    /// splinter.insert(100);
194    /// splinter.insert(50);
195    /// splinter.insert(200);
196    ///
197    /// assert_eq!(splinter.select(0), Some(50));   // Smallest element
198    /// assert_eq!(splinter.select(1), Some(100));  // Second smallest
199    /// assert_eq!(splinter.select(2), Some(200));  // Largest element
200    /// assert_eq!(splinter.select(3), None);       // Out of bounds
201    /// ```
202    #[inline]
203    fn select(&self, idx: usize) -> Option<u32> {
204        self.0.select(idx)
205    }
206
207    /// Returns the largest element in this splinter, or `None` if it's empty.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
213    ///
214    /// let mut splinter = Splinter::EMPTY;
215    /// assert_eq!(splinter.last(), None);
216    ///
217    /// splinter.insert(100);
218    /// splinter.insert(50);
219    /// splinter.insert(200);
220    ///
221    /// assert_eq!(splinter.last(), Some(200));
222    /// ```
223    #[inline]
224    fn last(&self) -> Option<u32> {
225        self.0.last()
226    }
227
228    /// Returns an iterator over all elements in ascending order.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use splinter_rs::{Splinter, PartitionRead, PartitionWrite};
234    ///
235    /// let mut splinter = Splinter::EMPTY;
236    /// splinter.insert(300);
237    /// splinter.insert(100);
238    /// splinter.insert(200);
239    ///
240    /// let values: Vec<u32> = splinter.iter().collect();
241    /// assert_eq!(values, vec![100, 200, 300]);
242    /// ```
243    #[inline]
244    fn iter(&self) -> impl Iterator<Item = u32> {
245        self.0.iter()
246    }
247}
248
249impl PartitionWrite<High> for Splinter {
250    /// Inserts a value into this splinter.
251    ///
252    /// Returns `true` if the value was newly inserted, or `false` if it was already present.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use splinter_rs::{Splinter, PartitionWrite, PartitionRead};
258    ///
259    /// let mut splinter = Splinter::EMPTY;
260    ///
261    /// // First insertion returns true
262    /// assert!(splinter.insert(42));
263    /// assert_eq!(splinter.cardinality(), 1);
264    ///
265    /// // Second insertion of same value returns false
266    /// assert!(!splinter.insert(42));
267    /// assert_eq!(splinter.cardinality(), 1);
268    ///
269    /// // Different value returns true
270    /// assert!(splinter.insert(100));
271    /// assert_eq!(splinter.cardinality(), 2);
272    /// ```
273    #[inline]
274    fn insert(&mut self, value: u32) -> bool {
275        self.0.insert(value)
276    }
277
278    /// Removes a value from this splinter.
279    ///
280    /// Returns `true` if the value was present and removed, or `false` if it was not present.
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// use splinter_rs::{Splinter, PartitionWrite, PartitionRead};
286    ///
287    /// let mut splinter = Splinter::EMPTY;
288    /// splinter.insert(42);
289    /// splinter.insert(100);
290    /// assert_eq!(splinter.cardinality(), 2);
291    ///
292    /// // Remove existing value
293    /// assert!(splinter.remove(42));
294    /// assert_eq!(splinter.cardinality(), 1);
295    /// assert!(!splinter.contains(42));
296    /// assert!(splinter.contains(100));
297    ///
298    /// // Remove non-existent value
299    /// assert!(!splinter.remove(999));
300    /// assert_eq!(splinter.cardinality(), 1);
301    /// ```
302    #[inline]
303    fn remove(&mut self, value: u32) -> bool {
304        self.0.remove(value)
305    }
306}
307
308impl Encodable for Splinter {
309    fn encoded_size(&self) -> usize {
310        self.0.encoded_size() + std::mem::size_of::<Footer>()
311    }
312
313    fn encode<B: bytes::BufMut>(&self, encoder: &mut Encoder<B>) {
314        self.0.encode(encoder);
315        encoder.write_footer();
316    }
317}
318
319impl Optimizable for Splinter {
320    #[inline]
321    fn optimize(&mut self) {
322        self.0.optimize();
323    }
324}
325
326impl Merge for Splinter {
327    fn merge(&mut self, rhs: &Self) {
328        self.0.merge(&rhs.0)
329    }
330}
331
332impl<B: Deref<Target = [u8]>> Merge<SplinterRef<B>> for Splinter {
333    fn merge(&mut self, rhs: &SplinterRef<B>) {
334        self.0.merge(&rhs.load_unchecked())
335    }
336}
337
338impl<B: Deref<Target = [u8]>> Merge<CowSplinter<B>> for Splinter {
339    fn merge(&mut self, rhs: &CowSplinter<B>) {
340        match rhs {
341            CowSplinter::Ref(splinter_ref) => self.merge(splinter_ref),
342            CowSplinter::Owned(splinter) => self.merge(splinter),
343        }
344    }
345}
346
347impl Cut for Splinter {
348    type Out = Self;
349
350    fn cut(&mut self, rhs: &Self) -> Self::Out {
351        Self(self.0.cut(&rhs.0))
352    }
353}
354
355impl<B: Deref<Target = [u8]>> Cut<SplinterRef<B>> for Splinter {
356    type Out = Self;
357
358    fn cut(&mut self, rhs: &SplinterRef<B>) -> Self::Out {
359        Self(self.0.cut(&rhs.load_unchecked()))
360    }
361}
362
363impl<B: Deref<Target = [u8]>> Cut<CowSplinter<B>> for Splinter {
364    type Out = Self;
365
366    fn cut(&mut self, rhs: &CowSplinter<B>) -> Self::Out {
367        match rhs {
368            CowSplinter::Ref(splinter_ref) => self.cut(splinter_ref),
369            CowSplinter::Owned(splinter) => self.cut(splinter),
370        }
371    }
372}
373
374impl<B: Deref<Target = [u8]>> PartialEq<SplinterRef<B>> for Splinter {
375    #[inline]
376    fn eq(&self, other: &SplinterRef<B>) -> bool {
377        self.0 == other.load_unchecked()
378    }
379}
380
381impl<B: Deref<Target = [u8]>> PartialEq<CowSplinter<B>> for Splinter {
382    fn eq(&self, other: &CowSplinter<B>) -> bool {
383        match other {
384            CowSplinter::Ref(splinter_ref) => self.eq(splinter_ref),
385            CowSplinter::Owned(splinter) => self.eq(splinter),
386        }
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use crate::{
394        codec::Encodable,
395        level::Level,
396        testutil::{SetGen, mksplinter, ratio_to_marks, test_partition_read, test_partition_write},
397        traits::Optimizable,
398    };
399    use itertools::Itertools;
400    use quickcheck::TestResult;
401    use quickcheck_macros::quickcheck;
402    use roaring::RoaringBitmap;
403
404    #[test]
405    fn test_sanity() {
406        let mut splinter = Splinter::EMPTY;
407
408        assert!(splinter.insert(1));
409        assert!(!splinter.insert(1));
410        assert!(splinter.contains(1));
411
412        let values = [1024, 123, 16384];
413        for v in values {
414            assert!(splinter.insert(v));
415            assert!(splinter.contains(v));
416            assert!(!splinter.contains(v + 1));
417        }
418
419        for i in 0..8192 + 10 {
420            splinter.insert(i);
421        }
422
423        splinter.optimize();
424
425        dbg!(&splinter);
426
427        let expected = splinter.iter().collect_vec();
428        test_partition_read(&splinter, &expected);
429        test_partition_write(&mut splinter);
430    }
431
432    #[test]
433    fn test_wat() {
434        let mut set_gen = SetGen::new(0xDEAD_BEEF);
435        let set = set_gen.random_max(64, 4096);
436        let baseline_size = set.len() * 4;
437
438        let mut splinter = Splinter::from_iter(set.iter().copied());
439        splinter.optimize();
440
441        dbg!(&splinter, splinter.encoded_size(), baseline_size, set.len());
442        itertools::assert_equal(splinter.iter(), set.into_iter());
443    }
444
445    #[quickcheck]
446    fn test_splinter_read_quickcheck(set: Vec<u32>) -> TestResult {
447        let expected = set.iter().copied().sorted().dedup().collect_vec();
448        test_partition_read(&Splinter::from_iter(set), &expected);
449        TestResult::passed()
450    }
451
452    #[quickcheck]
453    fn test_splinter_write_quickcheck(set: Vec<u32>) -> TestResult {
454        let mut splinter = Splinter::from_iter(set);
455        test_partition_write(&mut splinter);
456        TestResult::passed()
457    }
458
459    #[quickcheck]
460    fn test_splinter_quickcheck(set: Vec<u32>) -> bool {
461        let splinter = mksplinter(&set);
462        if set.is_empty() {
463            !splinter.contains(123)
464        } else {
465            let lookup = set[set.len() / 3];
466            splinter.contains(lookup)
467        }
468    }
469
470    #[quickcheck]
471    fn test_splinter_opt_quickcheck(set: Vec<u32>) -> bool {
472        let mut splinter = mksplinter(&set);
473        splinter.optimize();
474        if set.is_empty() {
475            !splinter.contains(123)
476        } else {
477            let lookup = set[set.len() / 3];
478            splinter.contains(lookup)
479        }
480    }
481
482    #[test]
483    fn test_expected_compression() {
484        fn to_roaring(set: impl Iterator<Item = u32>) -> Vec<u8> {
485            let mut buf = std::io::Cursor::new(Vec::new());
486            let mut bmp = RoaringBitmap::from_sorted_iter(set).unwrap();
487            bmp.optimize();
488            bmp.serialize_into(&mut buf).unwrap();
489            buf.into_inner()
490        }
491
492        struct Report {
493            name: String,
494            baseline: usize,
495            //        (actual, expected)
496            splinter: (usize, usize),
497            roaring: (usize, usize),
498
499            splinter_lz4: usize,
500            roaring_lz4: usize,
501        }
502
503        let mut reports = vec![];
504
505        let mut run_test = |name: &str,
506                            set: Vec<u32>,
507                            expected_set_size: usize,
508                            expected_splinter: usize,
509                            expected_roaring: usize| {
510            assert_eq!(set.len(), expected_set_size, "Set size mismatch");
511
512            let mut splinter = Splinter::from_iter(set.clone());
513            splinter.optimize();
514            itertools::assert_equal(splinter.iter(), set.iter().copied());
515
516            test_partition_read(&splinter, &set);
517            test_partition_write(&mut splinter.clone());
518
519            let expected_size = splinter.encoded_size();
520            let splinter = splinter.encode_to_bytes();
521
522            assert_eq!(
523                splinter.len(),
524                expected_size,
525                "actual encoded size does not match declared encoded size"
526            );
527
528            let roaring = to_roaring(set.iter().copied());
529
530            let splinter_lz4 = lz4::block::compress(&splinter, None, false).unwrap();
531            let roaring_lz4 = lz4::block::compress(&roaring, None, false).unwrap();
532
533            // verify round trip
534            assert_eq!(
535                splinter,
536                lz4::block::decompress(&splinter_lz4, Some(splinter.len() as i32)).unwrap()
537            );
538            assert_eq!(
539                roaring,
540                lz4::block::decompress(&roaring_lz4, Some(roaring.len() as i32)).unwrap()
541            );
542
543            reports.push(Report {
544                name: name.to_owned(),
545                baseline: set.len() * std::mem::size_of::<u32>(),
546                splinter: (splinter.len(), expected_splinter),
547                roaring: (roaring.len(), expected_roaring),
548
549                splinter_lz4: splinter_lz4.len(),
550                roaring_lz4: roaring_lz4.len(),
551            });
552        };
553
554        let mut set_gen = SetGen::new(0xDEAD_BEEF);
555
556        // empty splinter
557        run_test("empty", vec![], 0, 13, 8);
558
559        // 1 element in set
560        let set = set_gen.distributed(1, 1, 1, 1);
561        run_test("1 element", set, 1, 21, 18);
562
563        // 1 fully dense block
564        let set = set_gen.distributed(1, 1, 1, 256);
565        run_test("1 dense block", set, 256, 25, 15);
566
567        // 1 half full block
568        let set = set_gen.distributed(1, 1, 1, 128);
569        run_test("1 half full block", set, 128, 63, 255);
570
571        // 1 sparse block
572        let set = set_gen.distributed(1, 1, 1, 16);
573        run_test("1 sparse block", set, 16, 48, 48);
574
575        // 8 half full blocks
576        let set = set_gen.distributed(1, 1, 8, 128);
577        run_test("8 half full blocks", set, 1024, 315, 2003);
578
579        // 8 sparse blocks
580        let set = set_gen.distributed(1, 1, 8, 2);
581        run_test("8 sparse blocks", set, 16, 60, 48);
582
583        // 64 half full blocks
584        let set = set_gen.distributed(4, 4, 4, 128);
585        run_test("64 half full blocks", set, 8192, 2442, 16452);
586
587        // 64 sparse blocks
588        let set = set_gen.distributed(4, 4, 4, 2);
589        run_test("64 sparse blocks", set, 128, 410, 392);
590
591        // 256 half full blocks
592        let set = set_gen.distributed(4, 8, 8, 128);
593        run_test("256 half full blocks", set, 32768, 9450, 65580);
594
595        // 256 sparse blocks
596        let set = set_gen.distributed(4, 8, 8, 2);
597        run_test("256 sparse blocks", set, 512, 1290, 1288);
598
599        // 512 half full blocks
600        let set = set_gen.distributed(8, 8, 8, 128);
601        run_test("512 half full blocks", set, 65536, 18886, 130810);
602
603        // 512 sparse blocks
604        let set = set_gen.distributed(8, 8, 8, 2);
605        run_test("512 sparse blocks", set, 1024, 2566, 2568);
606
607        // the rest of the compression tests use 4k elements
608        let elements = 4096;
609
610        // fully dense splinter
611        let set = set_gen.distributed(1, 1, 16, 256);
612        run_test("fully dense", set, elements, 80, 63);
613
614        // 128 elements per block; dense partitions
615        let set = set_gen.distributed(1, 1, 32, 128);
616        run_test("128/block; dense", set, elements, 1179, 8208);
617
618        // 32 elements per block; dense partitions
619        let set = set_gen.distributed(1, 1, 128, 32);
620        run_test("32/block; dense", set, elements, 4539, 8208);
621
622        // 16 element per block; dense low partitions
623        let set = set_gen.distributed(1, 1, 256, 16);
624        run_test("16/block; dense", set, elements, 5147, 8208);
625
626        // 128 elements per block; sparse mid partitions
627        let set = set_gen.distributed(1, 32, 1, 128);
628        run_test("128/block; sparse mid", set, elements, 1365, 8282);
629
630        // 128 elements per block; sparse high partitions
631        let set = set_gen.distributed(32, 1, 1, 128);
632        run_test("128/block; sparse high", set, elements, 1582, 8224);
633
634        // 1 element per block; sparse mid partitions
635        let set = set_gen.distributed(1, 256, 16, 1);
636        run_test("1/block; sparse mid", set, elements, 9749, 10248);
637
638        // 1 element per block; sparse high partitions
639        let set = set_gen.distributed(256, 16, 1, 1);
640        run_test("1/block; sparse high", set, elements, 14350, 40968);
641
642        // 1/block; spread low
643        let set = set_gen.dense(1, 16, 256, 1);
644        run_test("1/block; spread low", set, elements, 8325, 8328);
645
646        // each partition is dense
647        let set = set_gen.dense(8, 8, 8, 8);
648        run_test("dense throughout", set, elements, 4113, 2700);
649
650        // the lowest partitions are dense
651        let set = set_gen.dense(1, 1, 64, 64);
652        run_test("dense low", set, elements, 529, 267);
653
654        // the mid and low partitions are dense
655        let set = set_gen.dense(1, 32, 16, 8);
656        run_test("dense mid/low", set, elements, 4113, 2376);
657
658        let random_cases = [
659            // random sets drawing from the enire u32 range
660            (32, High::MAX_LEN, 145, 328),
661            (256, High::MAX_LEN, 1041, 2544),
662            (1024, High::MAX_LEN, 4113, 10168),
663            (4096, High::MAX_LEN, 14350, 40056),
664            (16384, High::MAX_LEN, 51214, 148656),
665            (65536, High::MAX_LEN, 198670, 461288),
666            // random sets with values < 65536
667            (32, 65536, 92, 80),
668            (256, 65536, 540, 528),
669            (1024, 65536, 2071, 2064),
670            (4096, 65536, 5147, 8208),
671            (65536, 65536, 25, 15),
672            // small sets with values < 1024
673            (8, 1024, 49, 32),
674            (16, 1024, 60, 48),
675            (32, 1024, 79, 80),
676            (64, 1024, 111, 144),
677            (128, 1024, 168, 272),
678        ];
679
680        for (count, max, s, r) in random_cases {
681            let name = if max == High::MAX_LEN {
682                format!("random/{count}")
683            } else {
684                format!("random/{count}/{max}")
685            };
686            run_test(&name, set_gen.random_max(count, max), count, s, r);
687        }
688
689        let mut fail_test = false;
690
691        println!("{}", "-".repeat(83));
692        println!(
693            "{:30} {:12} {:>6} {:>10} {:>10} {:>10}",
694            "test", "bitmap", "size", "expected", "relative", "ok"
695        );
696        for report in &reports {
697            println!(
698                "{:30} {:12} {:6} {:10} {:>10} {:>10}",
699                report.name,
700                "Splinter",
701                report.splinter.0,
702                report.splinter.1,
703                "1.00",
704                if report.splinter.0 == report.splinter.1 {
705                    "ok"
706                } else {
707                    fail_test = true;
708                    "FAIL"
709                }
710            );
711
712            let diff = report.roaring.0 as f64 / report.splinter.0 as f64;
713            let ok_status = if report.roaring.0 != report.roaring.1 {
714                fail_test = true;
715                "FAIL".into()
716            } else {
717                ratio_to_marks(diff)
718            };
719            println!(
720                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
721                "", "Roaring", report.roaring.0, report.roaring.1, diff, ok_status
722            );
723
724            let diff = report.splinter_lz4 as f64 / report.splinter.0 as f64;
725            println!(
726                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
727                "",
728                "Splinter LZ4",
729                report.splinter_lz4,
730                report.splinter_lz4,
731                diff,
732                ratio_to_marks(diff)
733            );
734
735            let diff = report.roaring_lz4 as f64 / report.splinter_lz4 as f64;
736            println!(
737                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
738                "",
739                "Roaring LZ4",
740                report.roaring_lz4,
741                report.roaring_lz4,
742                diff,
743                ratio_to_marks(diff)
744            );
745
746            let diff = report.baseline as f64 / report.splinter.0 as f64;
747            println!(
748                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
749                "",
750                "Baseline",
751                report.baseline,
752                report.baseline,
753                diff,
754                ratio_to_marks(diff)
755            );
756        }
757
758        // calculate average compression ratio (splinter_lz4 / splinter)
759        let avg_ratio = reports
760            .iter()
761            .map(|r| r.splinter_lz4 as f64 / r.splinter.0 as f64)
762            .sum::<f64>()
763            / reports.len() as f64;
764
765        println!("average compression ratio (splinter_lz4 / splinter): {avg_ratio:.2}");
766
767        assert!(!fail_test, "compression test failed");
768    }
769}