diff --git a/CHANGELOG.md b/CHANGELOG.md index f324004..ac0146c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Midpoint-based `mean()` on the borrowed `CumulativeROHistogramRef` / + `CumulativeROHistogram32Ref` views. The mean is stored on the view + (computed once at construction, or carried over from the owned + histogram's cached value), so `mean()` is a cheap field access exposed + just like `count()` — no per-call streaming computation — letting a + zero-alloc reducer fold it in directly. Returns `None` for an empty + histogram. + +### Changed + +- `CumulativeROHistogramRef` / `CumulativeROHistogram32Ref` no longer + implement `Eq` (they now carry an `f64` mean); `PartialEq` is retained. + ## [1.4.0] - 2026-05-19 ### Added diff --git a/Cargo.toml b/Cargo.toml index 75e2ead..04f9f34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "histogram" -version = "1.4.0" +version = "1.5.0-alpha.1" edition = "2024" authors = ["Brian Martin ", "Yao Yue "] license = "MIT OR Apache-2.0" diff --git a/src/cumulative.rs b/src/cumulative.rs index 2052b6a..3b388e0 100644 --- a/src/cumulative.rs +++ b/src/cumulative.rs @@ -152,7 +152,7 @@ macro_rules! define_cumulative_histogram { /// Returns a borrowed view over this histogram's storage. pub fn as_ref(&self) -> $ref_name<'_> { - $ref_name::from_parts_unchecked(self.config, &self.index, &self.count) + $ref_name::from_parts_with_mean(self.config, &self.index, &self.count, self.mean) } } @@ -317,11 +317,14 @@ macro_rules! define_cumulative_histogram { /// /// The type is [`Copy`] — passing it around is cheap. Use /// `as_ref()` on the owned type or the `From<&Owned>` impl to obtain one. - #[derive(Clone, Copy, Debug, PartialEq, Eq)] + // `mean` is an `f64`, so `Eq` cannot be derived for this type. + #[derive(Clone, Copy, Debug, PartialEq)] pub struct $ref_name<'a> { config: Config, index: &'a [u32], count: &'a [$count], + /// Midpoint-estimated mean; `None` when empty. + mean: Option, } impl<'a> $ref_name<'a> { @@ -370,10 +373,12 @@ macro_rules! define_cumulative_histogram { count: &'a [$count], ) -> Result { Self::validate(&config, index, count)?; + let mean = Self::compute_mean(&config, index, count); Ok(Self { config, index, count, + mean, }) } @@ -387,11 +392,29 @@ macro_rules! define_cumulative_histogram { config: Config, index: &'a [u32], count: &'a [$count], + ) -> Self { + let mean = Self::compute_mean(&config, index, count); + Self { + config, + index, + count, + mean, + } + } + + /// Borrowed view with a precomputed mean; used by the owned + /// type's `as_ref()` / `From` impls, which already cache it. + fn from_parts_with_mean( + config: Config, + index: &'a [u32], + count: &'a [$count], + mean: Option, ) -> Self { Self { config, index, count, + mean, } } @@ -477,6 +500,14 @@ macro_rules! define_cumulative_histogram { ::quantile(self, quantile) } + /// Returns the midpoint-estimated mean, or `None` if empty. + /// + /// Stored on the view, so this is a cheap field access like + /// [`count`](Self::count) — no per-call computation. + pub fn mean(&self) -> Option { + self.mean + } + /// Computes the mean of all observations using bucket midpoints. /// /// `count` holds cumulative counts. Returns `None` when there are @@ -587,7 +618,7 @@ macro_rules! define_cumulative_histogram { impl<'a> From<&'a $name> for $ref_name<'a> { fn from(h: &'a $name) -> Self { - Self::from_parts_unchecked(h.config(), h.index(), h.count()) + Self::from_parts_with_mean(h.config(), h.index(), h.count(), h.mean()) } } @@ -1220,6 +1251,32 @@ mod tests { assert_eq!(owned.quantiles(qs).unwrap(), r.quantiles(qs).unwrap()); } + #[test] + fn ref_mean_parity_u64() { + let config = Config::new(7, 32).unwrap(); + let owned = + CumulativeROHistogram::from_parts(config, vec![0, 1, 2], vec![2u64, 5, 10]).unwrap(); + let r = CumulativeROHistogramRef::from(&owned); + assert_eq!(owned.mean(), r.mean()); + assert!((r.mean().unwrap() - 1.3).abs() < 1e-9); + } + + #[test] + fn ref_mean_parity_u32() { + let config = Config::new(7, 32).unwrap(); + let owned = + CumulativeROHistogram32::from_parts(config, vec![0, 1, 2], vec![2u32, 5, 10]).unwrap(); + let r = CumulativeROHistogram32Ref::from(&owned); + assert_eq!(owned.mean(), r.mean()); + } + + #[test] + fn ref_mean_empty_is_none() { + let config = Config::new(7, 32).unwrap(); + let r = CumulativeROHistogramRef::from_parts(config, &[], &[]).unwrap(); + assert_eq!(r.mean(), None); + } + #[test] fn ref_sample_quantiles_trait() { use crate::quantile::SampleQuantiles;