From 66454f12f37baa77d803748dc7e966426269936f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 17:28:33 +0000 Subject: [PATCH 1/3] Add mean() to borrowed CumulativeRO Ref views Exposes a public, non-allocating mean() on CumulativeROHistogramRef and CumulativeROHistogram32Ref so a zero-alloc streaming reducer holding a borrowed Ref can fold the midpoint-based mean in directly. The owned type caches the mean at construction; the borrowed view computes it from the borrowed slices on each call. https://claude.ai/code/session_01LDxW544z6m39mY9SkMh9XD --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 2 +- src/cumulative.rs | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f324004..29bda76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ 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. Unlike the owned type, the borrowed + view does not cache the mean; it is computed from the borrowed slices on + each call without allocating, so a zero-alloc streaming reducer holding a + borrowed `Ref` can fold it in directly. Returns `None` for an empty + histogram. + ## [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..58c83af 100644 --- a/src/cumulative.rs +++ b/src/cumulative.rs @@ -477,6 +477,17 @@ macro_rules! define_cumulative_histogram { ::quantile(self, quantile) } + /// Returns the mean of all observations, estimated using bucket + /// midpoints, or `None` if the histogram is empty. + /// + /// Unlike the owned histogram, the borrowed view does not cache + /// the mean; it is computed from the borrowed slices on each call + /// without allocating, so a zero-alloc streaming reducer holding a + /// borrowed `Ref` can fold it in directly. + pub fn mean(&self) -> Option { + Self::compute_mean(&self.config, self.index, self.count) + } + /// Computes the mean of all observations using bucket midpoints. /// /// `count` holds cumulative counts. Returns `None` when there are @@ -1220,6 +1231,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; From 7d27562c70261fd63a844376b301129d01a493ed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 17:34:36 +0000 Subject: [PATCH 2/3] Store mean on CumulativeRO Ref instead of recomputing per call Expose mean() as a cheap field access on the borrowed Ref views, mirroring count(), instead of streaming the midpoint computation on every call. The owned type's as_ref()/From impls pass their already-cached mean through, so the hot path does no recomputation; slice-only constructors compute it once. The Ref types now carry an f64 and so no longer derive Eq (PartialEq kept). https://claude.ai/code/session_01LDxW544z6m39mY9SkMh9XD --- CHANGELOG.md | 14 ++++++++++---- src/cumulative.rs | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29bda76..ac0146c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Midpoint-based `mean()` on the borrowed `CumulativeROHistogramRef` / - `CumulativeROHistogram32Ref` views. Unlike the owned type, the borrowed - view does not cache the mean; it is computed from the borrowed slices on - each call without allocating, so a zero-alloc streaming reducer holding a - borrowed `Ref` can fold it in directly. Returns `None` for an empty + `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/src/cumulative.rs b/src/cumulative.rs index 58c83af..7b5a179 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,15 @@ 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], + /// Mean of all observations, estimated using bucket midpoints. + /// `None` when the histogram is empty. + mean: Option, } impl<'a> $ref_name<'a> { @@ -370,10 +374,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, }) } @@ -388,10 +394,29 @@ macro_rules! define_cumulative_histogram { index: &'a [u32], count: &'a [$count], ) -> Self { + let mean = Self::compute_mean(&config, index, count); Self { config, index, count, + mean, + } + } + + /// Creates a borrowed view with a precomputed mean, skipping both + /// validation and the mean computation. Used by the owned type's + /// `as_ref()` / `From` impls, which already cache the mean. + fn from_parts_with_mean( + config: Config, + index: &'a [u32], + count: &'a [$count], + mean: Option, + ) -> Self { + Self { + config, + index, + count, + mean, } } @@ -480,12 +505,14 @@ macro_rules! define_cumulative_histogram { /// Returns the mean of all observations, estimated using bucket /// midpoints, or `None` if the histogram is empty. /// - /// Unlike the owned histogram, the borrowed view does not cache - /// the mean; it is computed from the borrowed slices on each call - /// without allocating, so a zero-alloc streaming reducer holding a - /// borrowed `Ref` can fold it in directly. + /// The mean is stored on the view (computed once at construction, + /// or carried over from the owned histogram's cached value), so + /// this is a cheap field access — exposed just like [`count`] — + /// and a zero-alloc streaming reducer can fold it in directly. + /// + /// [`count`]: Self::count pub fn mean(&self) -> Option { - Self::compute_mean(&self.config, self.index, self.count) + self.mean } /// Computes the mean of all observations using bucket midpoints. @@ -598,7 +625,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()) } } From 65b86d614ee2df49f12c2c1340981d27ea402d22 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 17:37:21 +0000 Subject: [PATCH 3/3] Tighten doc comments on CumulativeRO Ref mean https://claude.ai/code/session_01LDxW544z6m39mY9SkMh9XD --- src/cumulative.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/cumulative.rs b/src/cumulative.rs index 7b5a179..3b388e0 100644 --- a/src/cumulative.rs +++ b/src/cumulative.rs @@ -323,8 +323,7 @@ macro_rules! define_cumulative_histogram { config: Config, index: &'a [u32], count: &'a [$count], - /// Mean of all observations, estimated using bucket midpoints. - /// `None` when the histogram is empty. + /// Midpoint-estimated mean; `None` when empty. mean: Option, } @@ -403,9 +402,8 @@ macro_rules! define_cumulative_histogram { } } - /// Creates a borrowed view with a precomputed mean, skipping both - /// validation and the mean computation. Used by the owned type's - /// `as_ref()` / `From` impls, which already cache the 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], @@ -502,15 +500,10 @@ macro_rules! define_cumulative_histogram { ::quantile(self, quantile) } - /// Returns the mean of all observations, estimated using bucket - /// midpoints, or `None` if the histogram is empty. - /// - /// The mean is stored on the view (computed once at construction, - /// or carried over from the owned histogram's cached value), so - /// this is a cheap field access — exposed just like [`count`] — - /// and a zero-alloc streaming reducer can fold it in directly. + /// Returns the midpoint-estimated mean, or `None` if empty. /// - /// [`count`]: Self::count + /// 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 }