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        testutil::{SetGen, mksplinter, ratio_to_marks, test_partition_read, test_partition_write},
396        traits::Optimizable,
397    };
398    use itertools::Itertools;
399    use quickcheck::TestResult;
400    use quickcheck_macros::quickcheck;
401    use roaring::RoaringBitmap;
402
403    #[test]
404    fn test_sanity() {
405        let mut splinter = Splinter::EMPTY;
406
407        assert!(splinter.insert(1));
408        assert!(!splinter.insert(1));
409        assert!(splinter.contains(1));
410
411        let values = [1024, 123, 16384];
412        for v in values {
413            assert!(splinter.insert(v));
414            assert!(splinter.contains(v));
415            assert!(!splinter.contains(v + 1));
416        }
417
418        for i in 0..8192 + 10 {
419            splinter.insert(i);
420        }
421
422        splinter.optimize();
423
424        dbg!(&splinter);
425
426        let expected = splinter.iter().collect_vec();
427        test_partition_read(&splinter, &expected);
428        test_partition_write(&mut splinter);
429    }
430
431    #[test]
432    fn test_wat() {
433        let mut set_gen = SetGen::new(0xDEAD_BEEF);
434        let set = set_gen.distributed(4, 8, 16, 32);
435        let baseline_size = set.len() * 4;
436
437        let mut splinter = Splinter::from_iter(set.iter().copied());
438        splinter.optimize();
439
440        dbg!(&splinter, splinter.encoded_size(), baseline_size, set.len());
441        itertools::assert_equal(splinter.iter(), set.into_iter());
442    }
443
444    #[quickcheck]
445    fn test_splinter_read_quickcheck(set: Vec<u32>) -> TestResult {
446        let expected = set.iter().copied().sorted().dedup().collect_vec();
447        test_partition_read(&Splinter::from_iter(set), &expected);
448        TestResult::passed()
449    }
450
451    #[quickcheck]
452    fn test_splinter_write_quickcheck(set: Vec<u32>) -> TestResult {
453        let mut splinter = Splinter::from_iter(set);
454        test_partition_write(&mut splinter);
455        TestResult::passed()
456    }
457
458    #[quickcheck]
459    fn test_splinter_quickcheck(set: Vec<u32>) -> bool {
460        let splinter = mksplinter(&set);
461        if set.is_empty() {
462            !splinter.contains(123)
463        } else {
464            let lookup = set[set.len() / 3];
465            splinter.contains(lookup)
466        }
467    }
468
469    #[quickcheck]
470    fn test_splinter_opt_quickcheck(set: Vec<u32>) -> bool {
471        let mut splinter = mksplinter(&set);
472        splinter.optimize();
473        if set.is_empty() {
474            !splinter.contains(123)
475        } else {
476            let lookup = set[set.len() / 3];
477            splinter.contains(lookup)
478        }
479    }
480
481    #[test]
482    fn test_expected_compression() {
483        fn to_roaring(set: impl Iterator<Item = u32>) -> Vec<u8> {
484            let mut buf = std::io::Cursor::new(Vec::new());
485            let mut bmp = RoaringBitmap::from_sorted_iter(set).unwrap();
486            bmp.optimize();
487            bmp.serialize_into(&mut buf).unwrap();
488            buf.into_inner()
489        }
490
491        struct Report {
492            name: &'static str,
493            baseline: usize,
494            //        (actual, expected)
495            splinter: (usize, usize),
496            roaring: (usize, usize),
497
498            splinter_lz4: usize,
499            roaring_lz4: usize,
500        }
501
502        let mut reports = vec![];
503
504        let mut run_test = |name: &'static str,
505                            set: Vec<u32>,
506                            expected_set_size: usize,
507                            expected_splinter: usize,
508                            expected_roaring: usize| {
509            assert_eq!(set.len(), expected_set_size, "Set size mismatch");
510
511            let mut splinter = Splinter::from_iter(set.clone());
512            splinter.optimize();
513            itertools::assert_equal(splinter.iter(), set.iter().copied());
514
515            test_partition_read(&splinter, &set);
516            test_partition_write(&mut splinter.clone());
517
518            let expected_size = splinter.encoded_size();
519            let splinter = splinter.encode_to_bytes();
520
521            assert_eq!(
522                splinter.len(),
523                expected_size,
524                "actual encoded size does not match declared encoded size"
525            );
526
527            let roaring = to_roaring(set.iter().copied());
528
529            let splinter_lz4 = lz4::block::compress(&splinter, None, false).unwrap();
530            let roaring_lz4 = lz4::block::compress(&roaring, None, false).unwrap();
531
532            // verify round trip
533            assert_eq!(
534                splinter,
535                lz4::block::decompress(&splinter_lz4, Some(splinter.len() as i32)).unwrap()
536            );
537            assert_eq!(
538                roaring,
539                lz4::block::decompress(&roaring_lz4, Some(roaring.len() as i32)).unwrap()
540            );
541
542            reports.push(Report {
543                name,
544                baseline: set.len() * std::mem::size_of::<u32>(),
545                splinter: (splinter.len(), expected_splinter),
546                roaring: (roaring.len(), expected_roaring),
547
548                splinter_lz4: splinter_lz4.len(),
549                roaring_lz4: roaring_lz4.len(),
550            });
551        };
552
553        let mut set_gen = SetGen::new(0xDEAD_BEEF);
554
555        // empty splinter
556        run_test("empty", vec![], 0, 13, 8);
557
558        // 1 element in set
559        let set = set_gen.distributed(1, 1, 1, 1);
560        run_test("1 element", set, 1, 21, 18);
561
562        // 1 fully dense block
563        let set = set_gen.distributed(1, 1, 1, 256);
564        run_test("1 dense block", set, 256, 25, 15);
565
566        // 1 half full block
567        let set = set_gen.distributed(1, 1, 1, 128);
568        run_test("1 half full block", set, 128, 63, 255);
569
570        // 1 sparse block
571        let set = set_gen.distributed(1, 1, 1, 16);
572        run_test("1 sparse block", set, 16, 81, 48);
573
574        // 8 half full blocks
575        let set = set_gen.distributed(1, 1, 8, 128);
576        run_test("8 half full blocks", set, 1024, 315, 2003);
577
578        // 8 sparse blocks
579        let set = set_gen.distributed(1, 1, 8, 2);
580        run_test("8 sparse blocks", set, 16, 81, 48);
581
582        // 64 half full blocks
583        let set = set_gen.distributed(4, 4, 4, 128);
584        run_test("64 half full blocks", set, 8192, 2442, 16452);
585
586        // 64 sparse blocks
587        let set = set_gen.distributed(4, 4, 4, 2);
588        run_test("64 sparse blocks", set, 128, 434, 392);
589
590        // 256 half full blocks
591        let set = set_gen.distributed(4, 8, 8, 128);
592        run_test("256 half full blocks", set, 32768, 9450, 65580);
593
594        // 256 sparse blocks
595        let set = set_gen.distributed(4, 8, 8, 2);
596        run_test("256 sparse blocks", set, 512, 1290, 1288);
597
598        // 512 half full blocks
599        let set = set_gen.distributed(8, 8, 8, 128);
600        run_test("512 half full blocks", set, 65536, 18886, 130810);
601
602        // 512 sparse blocks
603        let set = set_gen.distributed(8, 8, 8, 2);
604        run_test("512 sparse blocks", set, 1024, 2566, 2568);
605
606        // the rest of the compression tests use 4k elements
607        let elements = 4096;
608
609        // fully dense splinter
610        let set = set_gen.distributed(1, 1, 16, 256);
611        run_test("fully dense", set, elements, 80, 63);
612
613        // 128 elements per block; dense partitions
614        let set = set_gen.distributed(1, 1, 32, 128);
615        run_test("128/block; dense", set, elements, 1179, 8208);
616
617        // 32 elements per block; dense partitions
618        let set = set_gen.distributed(1, 1, 128, 32);
619        run_test("32/block; dense", set, elements, 4539, 8208);
620
621        // 16 element per block; dense low partitions
622        let set = set_gen.distributed(1, 1, 256, 16);
623        run_test("16/block; dense", set, elements, 5147, 8208);
624
625        // 128 elements per block; sparse mid partitions
626        let set = set_gen.distributed(1, 32, 1, 128);
627        run_test("128/block; sparse mid", set, elements, 1365, 8282);
628
629        // 128 elements per block; sparse high partitions
630        let set = set_gen.distributed(32, 1, 1, 128);
631        run_test("128/block; sparse high", set, elements, 1582, 8224);
632
633        // 1 element per block; sparse mid partitions
634        let set = set_gen.distributed(1, 256, 16, 1);
635        run_test("1/block; sparse mid", set, elements, 9749, 10248);
636
637        // 1 element per block; sparse high partitions
638        let set = set_gen.distributed(256, 16, 1, 1);
639        run_test("1/block; sparse high", set, elements, 14350, 40968);
640
641        // 1/block; spread low
642        let set = set_gen.dense(1, 16, 256, 1);
643        run_test("1/block; spread low", set, elements, 8325, 8328);
644
645        // each partition is dense
646        let set = set_gen.dense(8, 8, 8, 8);
647        run_test("dense throughout", set, elements, 4113, 2700);
648
649        // the lowest partitions are dense
650        let set = set_gen.dense(1, 1, 64, 64);
651        run_test("dense low", set, elements, 529, 267);
652
653        // the mid and low partitions are dense
654        let set = set_gen.dense(1, 32, 16, 8);
655        run_test("dense mid/low", set, elements, 4113, 2376);
656
657        // fully random sets of varying sizes
658        run_test("random/32", set_gen.random(32), 32, 145, 328);
659        run_test("random/256", set_gen.random(256), 256, 1041, 2544);
660        run_test("random/1024", set_gen.random(1024), 1024, 4113, 10168);
661        run_test("random/4096", set_gen.random(4096), 4096, 14350, 40056);
662        run_test("random/16384", set_gen.random(16384), 16384, 51214, 148656);
663        run_test("random/65535", set_gen.random(65535), 65535, 198667, 461278);
664
665        let mut fail_test = false;
666
667        println!("{}", "-".repeat(83));
668        println!(
669            "{:30} {:12} {:>6} {:>10} {:>10} {:>10}",
670            "test", "bitmap", "size", "expected", "relative", "ok"
671        );
672        for report in &reports {
673            println!(
674                "{:30} {:12} {:6} {:10} {:>10} {:>10}",
675                report.name,
676                "Splinter",
677                report.splinter.0,
678                report.splinter.1,
679                "1.00",
680                if report.splinter.0 == report.splinter.1 {
681                    "ok"
682                } else {
683                    fail_test = true;
684                    "FAIL"
685                }
686            );
687
688            let diff = report.roaring.0 as f64 / report.splinter.0 as f64;
689            let ok_status = if report.roaring.0 != report.roaring.1 {
690                fail_test = true;
691                "FAIL".into()
692            } else {
693                ratio_to_marks(diff)
694            };
695            println!(
696                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
697                "", "Roaring", report.roaring.0, report.roaring.1, diff, ok_status
698            );
699
700            let diff = report.splinter_lz4 as f64 / report.splinter.0 as f64;
701            println!(
702                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
703                "",
704                "Splinter LZ4",
705                report.splinter_lz4,
706                report.splinter_lz4,
707                diff,
708                ratio_to_marks(diff)
709            );
710
711            let diff = report.roaring_lz4 as f64 / report.splinter_lz4 as f64;
712            println!(
713                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
714                "",
715                "Roaring LZ4",
716                report.roaring_lz4,
717                report.roaring_lz4,
718                diff,
719                ratio_to_marks(diff)
720            );
721
722            let diff = report.baseline as f64 / report.splinter.0 as f64;
723            println!(
724                "{:30} {:12} {:6} {:10} {:>10.2} {:>10}",
725                "",
726                "Baseline",
727                report.baseline,
728                report.baseline,
729                diff,
730                ratio_to_marks(diff)
731            );
732        }
733
734        // calculate average compression ratio (splinter_lz4 / splinter)
735        let avg_ratio = reports
736            .iter()
737            .map(|r| r.splinter_lz4 as f64 / r.splinter.0 as f64)
738            .sum::<f64>()
739            / reports.len() as f64;
740
741        println!("average compression ratio (splinter_lz4 / splinter): {avg_ratio:.2}");
742
743        assert!(!fail_test, "compression test failed");
744    }
745}