histogram 1.6.0

A collection of histogram data structures
Documentation
use histogram::{
    Config, CumulativeROHistogram, CumulativeROHistogram32, Error, Histogram, Histogram32,
};

macro_rules! transform_tests {
    ($module:ident, $owned:ident, $view:ident, $dense:ident, $count:ty) => {
        mod $module {
            use super::*;

            fn replay(gp: u8, max: u8, values: &[u64]) -> $owned {
                let mut dense = $dense::new(gp, max).unwrap();
                for &value in values {
                    dense.increment(value).unwrap();
                }
                $owned::from(&dense)
            }

            #[test]
            fn merge_matches_raw_replay_for_empty_disjoint_and_overlapping_windows() {
                let windows: &[&[u64]] = &[&[], &[0], &[1, 1, 17, 255], &[0, 17, 17, 256, 1023]];
                for left in windows {
                    for right in windows {
                        let a = replay(3, 10, left);
                        let b = replay(3, 10, right);
                        let before = (a.clone(), b.clone());
                        let values: Vec<_> = left.iter().chain(right.iter()).copied().collect();
                        let expected = replay(3, 10, &values);
                        let result = a.checked_add(&b).unwrap();
                        assert_eq!(result, expected);
                        assert_eq!(a.as_ref().checked_add(&b.as_ref()).unwrap(), expected);
                        assert_eq!(b.checked_add(&a).unwrap(), expected);
                        for q in [0.0, 0.5, 0.99, 1.0] {
                            assert_eq!(result.quantile_bucket(q), expected.quantile_bucket(q));
                        }
                        assert_eq!((a, b), before);
                    }
                }
            }

            #[test]
            fn downsample_matches_every_value_replayed_at_all_coarser_geometries() {
                let values: Vec<_> = (0..1024).chain((0..1024).step_by(7)).collect();
                for gp in 1..8 {
                    let input = replay(gp, 10, &values);
                    let before = input.clone();
                    for target in 0..gp {
                        let expected = replay(target, 10, &values);
                        assert_eq!(input.downsample(target).unwrap(), expected);
                        assert_eq!(input.as_ref().downsample(target).unwrap(), expected);
                        assert_eq!(
                            replay(gp, 10, &[]).downsample(target).unwrap(),
                            replay(target, 10, &[])
                        );
                    }
                    assert_eq!(input, before);
                }
            }

            #[test]
            fn transforms_cover_u64_value_range_boundaries() {
                let values = [
                    0,
                    1,
                    255,
                    256,
                    257,
                    1 << 32,
                    (1 << 63) - 1,
                    1 << 63,
                    u64::MAX,
                ];
                let a = replay(7, 64, &values);
                assert_eq!(a.downsample(0).unwrap(), replay(0, 64, &values));
                let doubled: Vec<_> = values.into_iter().chain(values).collect();
                assert_eq!(a.checked_add(&a).unwrap(), replay(7, 64, &doubled));
            }

            #[test]
            fn rejects_geometry_mismatch_even_for_empty_inputs() {
                let a = replay(3, 10, &[]);
                for b in [replay(2, 10, &[]), replay(3, 11, &[])] {
                    assert_eq!(a.checked_add(&b), Err(Error::IncompatibleParameters));
                    assert_eq!(
                        a.as_ref().checked_add(&b.as_ref()),
                        Err(Error::IncompatibleParameters)
                    );
                }
                for gp in [3, 4, 255] {
                    assert_eq!(a.downsample(gp), Err(Error::IncompatibleParameters));
                }
            }

            #[test]
            fn total_overflow_is_rejected_even_when_individual_buckets_fit() {
                let config = Config::new(3, 10).unwrap();
                let a = $owned::from_parts(config, vec![1], vec![<$count>::MAX]).unwrap();
                let b = $owned::from_parts(config, vec![2], vec![1]).unwrap();
                let before = (a.clone(), b.clone());
                assert_eq!(a.checked_add(&b), Err(Error::Overflow));
                assert_eq!(a.checked_add(&a), Err(Error::Overflow));
                assert_eq!(a.as_ref().checked_add(&b.as_ref()), Err(Error::Overflow));
                assert_eq!((a, b), before);
                let near = $owned::from_parts(config, vec![1], vec![<$count>::MAX - 1]).unwrap();
                let merged = near.checked_add(&before.1).unwrap();
                assert_eq!(merged.count(), &[<$count>::MAX - 1, <$count>::MAX]);
                assert_eq!(
                    merged.downsample(0).unwrap().total_count(),
                    <$count>::MAX as u64
                );
            }

            #[test]
            fn repeated_prefixes_are_zero_deltas_and_are_omitted_from_output() {
                let config = Config::new(3, 10).unwrap();
                let a = $owned::from_parts(config, vec![1, 2, 3], vec![2, 2, 5]).unwrap();
                let b = $owned::from_parts(config, vec![2, 4], vec![1, 4]).unwrap();
                let merged = a.checked_add(&b).unwrap();
                assert_eq!(merged.index(), &[1, 2, 3, 4]);
                assert_eq!(merged.count(), &[2, 3, 6, 9]);
                let empty = replay(3, 10, &[]);
                assert_eq!(a.checked_add(&empty).unwrap().index(), &[1, 3]);
                assert_eq!(a.downsample(2).unwrap().index(), &[1, 3]);
            }

            #[cfg(feature = "serde")]
            #[test]
            fn transforms_use_validated_midpoint_means_after_deserialization() {
                let original = replay(3, 10, &[17, 17, 35]);
                let mut encoded = serde_json::to_value(&original).unwrap();
                encoded["mean"] = serde_json::json!(123.456);
                let owned: $owned = serde_json::from_value(encoded).unwrap();
                let view = owned.as_ref();
                let empty = replay(3, 10, &[]);
                assert_eq!(
                    view.checked_add(&empty.as_ref()).unwrap().mean(),
                    original.mean()
                );
                let coarse = view.downsample(0).unwrap();
                assert_eq!(coarse.mean(), Some((23.5 * 2.0 + 47.5) / 3.0));
                assert_eq!(view.mean(), original.mean());
            }

            #[test]
            fn many_window_merge_then_downsample_matches_raw_replay() {
                for count in [2, 8, 64] {
                    let mut all = Vec::new();
                    let mut merged = replay(5, 12, &[]);
                    for window in 0..count {
                        let values: Vec<_> =
                            (0..64).map(|i| (i * 53 + window * 197) % 4096).collect();
                        merged = merged.checked_add(&replay(5, 12, &values)).unwrap();
                        all.extend(values);
                    }
                    assert_eq!(merged, replay(5, 12, &all));
                    assert_eq!(merged.downsample(2).unwrap(), replay(2, 12, &all));
                }
            }

            #[cfg(feature = "serde")]
            #[test]
            fn transformed_outputs_roundtrip_through_existing_serialization() {
                let a = replay(3, 10, &[17, 17, 35]);
                for output in [a.checked_add(&a).unwrap(), a.downsample(1).unwrap()] {
                    let bytes = serde_json::to_vec(&output).unwrap();
                    let decoded: $owned = serde_json::from_slice(&bytes).unwrap();
                    assert_eq!(decoded, output);
                }
            }
        }
    };
}

transform_tests!(
    wide,
    CumulativeROHistogram,
    CumulativeROHistogramRef,
    Histogram,
    u64
);
transform_tests!(
    narrow,
    CumulativeROHistogram32,
    CumulativeROHistogram32Ref,
    Histogram32,
    u32
);

#[test]
fn mixed_width_merge_requires_explicit_widening_and_checked_narrowing() {
    let config = Config::new(3, 10).unwrap();
    let narrow = CumulativeROHistogram32::from_parts(config, vec![1], vec![u32::MAX]).unwrap();
    let wide = CumulativeROHistogram::from_parts(config, vec![2], vec![1]).unwrap();
    let result = CumulativeROHistogram::from(&narrow)
        .checked_add(&wide)
        .unwrap();
    assert_eq!(result.total_count(), u32::MAX as u64 + 1);
    assert_eq!(
        CumulativeROHistogram32::try_from(&result),
        Err(Error::Overflow)
    );
}