This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git
The following commit(s) were added to refs/heads/main by this push:
new cc614c2 fix: distinguish empty sketch queries from invalid arguments
(#295)
cc614c2 is described below
commit cc614c2a4e59b3a062c5fa4aecd371327a654629
Author: tison <[email protected]>
AuthorDate: Tue Oct 20 21:25:05 2026 +0800
fix: distinguish empty sketch queries from invalid arguments (#295)
Signed-off-by: tison <[email protected]>
Co-authored-by: Jiawei Zhao <[email protected]>
---
CHANGELOG.md | 4 +-
CONTRIBUTING.md | 7 +
benchmarks/tdigest/aggregation.rs | 1 +
benchmarks/tdigest/compress.rs | 6 +-
benchmarks/tdigest/merge.rs | 22 +-
benchmarks/tdigest/query.rs | 12 +-
datasketches/src/kll/mod.rs | 12 +-
datasketches/src/kll/sketch.rs | 80 ++--
datasketches/src/kll/sorted_view.rs | 104 +++--
datasketches/src/req/mod.rs | 4 +-
datasketches/src/req/sketch.rs | 105 +++--
datasketches/src/req/sorted_view.rs | 144 ++++---
datasketches/src/tdigest/mod.rs | 8 +-
datasketches/src/tdigest/sketch.rs | 218 +++++-----
tests-integration/tests/kll_test/generic.rs | 12 +-
tests-integration/tests/kll_test/merge.rs | 1 +
tests-integration/tests/kll_test/query.rs | 157 ++++++--
tests-integration/tests/req_test/accuracy.rs | 8 +-
tests-integration/tests/req_test/bounds.rs | 4 +-
tests-integration/tests/req_test/core.rs | 44 ++-
tests-integration/tests/req_test/generic.rs | 5 +-
tests-integration/tests/req_test/merge.rs | 21 +-
tests-integration/tests/req_test/property.rs | 6 +-
tests-integration/tests/req_test/query.rs | 84 ++--
.../tests/req_test/sorted_view_api.rs | 96 +++--
tests-integration/tests/serde_tests/req.rs | 5 +-
tests-integration/tests/serde_tests/tdigest.rs | 59 ++-
tests-integration/tests/tdigest_test/property.rs | 18 +-
tests-integration/tests/tdigest_test/sketch.rs | 439 ++++++++++++++-------
29 files changed, 1092 insertions(+), 594 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cfc3df..2e4e3ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,11 +9,13 @@ All significant changes to this project will be documented in
this file.
* `BloomFilter::invert` is removed; use `BloomFilter::difference` for
approximate A-not-B. The result excludes items in the right filter, but hash
collisions can also remove items unique to the left filter.
* `FrequentItemsSketch::is_empty` now returns `false` when the stream weight
is nonzero, even if no items are retained. Use `num_active_items() == 0` to
test for zero retained items.
* `ReqSketch` queries now use `datasketches::common::SearchCriteria` instead
of `datasketches::req::SearchCriteria`. `SearchCriteria` no longer implements
`Default`; explicitly choose `Inclusive` or `Exclusive`.
+* `ReqSketch` and `req::SortedView` queries now distinguish empty sketches
from invalid arguments. `rank` returns `Option<f64>`; `quantile`, `quantiles`,
`cdf`, and `pmf`, where available, return `Result<Option<_>, Error>`. Handle
`None` for valid queries on an empty sketch, and propagate or handle
`InvalidArgument` for invalid ranks or split points, which are checked before
emptiness.
+* `TDigest` and `TDigestMut` rank, quantile, CDF, and PMF queries now return
`Result<Option<_>, Error>`. Invalid arguments return `InvalidArgument` instead
of panicking, including on empty digests. Propagate or handle the `Result`
before handling `None` for an empty digest; `min_value` and `max_value` still
return `Option<f64>`.
### New features
* Add `HllSketch::composite_estimate` and `HllUnion::composite_estimate` to
query cardinality without relying on HIP update history, matching Java's
`getCompositeEstimate()`.
-* `KllSketch` is now available behind the `kll` feature, with rank, quantile,
PMF, and CDF queries, merging, serialization, custom ordered item types, and a
`KllFloat` adapter for non-NaN floating-point values.
+* `KllSketch` is now available behind the `kll` feature, with rank, quantile,
PMF, and CDF queries, merging, serialization, custom ordered item types, and a
`KllFloat` adapter for non-NaN floating-point values. Queries distinguish an
empty sketch (`None`) from invalid arguments (`InvalidArgument`).
* Add `TDigestMut::quantiles` and `TDigest::quantiles` for querying several
ranks in one centroid scan while preserving input order.
* Add `FromIterator<TDigestMut>` as an alternative to incremental
`TDigestMut::merge` calls for combining owned partial sketches in a single
compression pass.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 841f850..8d9f5ba 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -80,6 +80,13 @@ cargo bench --package benchmarks --bench benchmarks --
cpc::serde
- Describe types with noun phrases and API behavior with third-person
present-tense verbs such as `Creates`, `Updates`, and `Returns`.
- End summary sentences with punctuation, and format Rust identifiers,
literals, and numeric ranges as inline code.
- Put contract sections and compatibility notes before examples. When
applicable, order sections as `# Errors`, `# Panics`, and `# Examples`. Include
only sections that describe an actual contract.
+- Document return values and error conditions on the relevant methods rather
than repeating them in crate or module overviews.
+- Keep imports visible in doctests. A trailing hidden `Ok(...)` may be used to
support `?`.
+
+## Query semantics
+
+- Validate query arguments before handling an empty sketch. Invalid arguments
return `InvalidArgument` regardless of whether the sketch is empty.
+- Return a meaningful result when one exists, such as zero, `false`, or an
empty collection. Otherwise, use `Option` for an absent result, combined with
`Result` when argument validation can fail.
## Visibility
diff --git a/benchmarks/tdigest/aggregation.rs
b/benchmarks/tdigest/aggregation.rs
index da17069..dd41823 100644
--- a/benchmarks/tdigest/aggregation.rs
+++ b/benchmarks/tdigest/aggregation.rs
@@ -127,4 +127,5 @@ fn final_quantile(mut merged: TDigestMut) -> Option<f64> {
TDigestMut::deserialize(black_box(&bytes))
.unwrap()
.quantile(black_box(0.9))
+ .unwrap()
}
diff --git a/benchmarks/tdigest/compress.rs b/benchmarks/tdigest/compress.rs
index 3fd4cb6..ead1900 100644
--- a/benchmarks/tdigest/compress.rs
+++ b/benchmarks/tdigest/compress.rs
@@ -31,14 +31,14 @@ fn initial_buffer(bencher: Bencher) {
bencher
.counter(ItemsCount::new(values.len()))
.with_inputs(|| digest.clone())
- .bench_local_values(|mut digest| black_box(digest.rank(0.5)));
+ .bench_local_values(|mut digest| black_box(digest.rank(0.5).unwrap()));
}
#[divan::bench]
fn unmerged_tail(bencher: Bencher) {
let values = values(3_280);
let mut digest = build_mut_digest(&values[..1_640]);
- black_box(digest.rank(0.5));
+ black_box(digest.rank(0.5).unwrap());
for &value in &values[1_640..] {
digest.update(value);
}
@@ -46,7 +46,7 @@ fn unmerged_tail(bencher: Bencher) {
bencher
.counter(ItemsCount::new(1_640_usize))
.with_inputs(|| digest.clone())
- .bench_local_values(|mut digest| black_box(digest.rank(0.5)));
+ .bench_local_values(|mut digest| black_box(digest.rank(0.5).unwrap()));
}
#[divan::bench]
diff --git a/benchmarks/tdigest/merge.rs b/benchmarks/tdigest/merge.rs
index 12f6f8b..d8ae273 100644
--- a/benchmarks/tdigest/merge.rs
+++ b/benchmarks/tdigest/merge.rs
@@ -34,8 +34,8 @@ fn merge(bencher: Bencher) {
let values = values(200_000);
let mut left = build_mut_digest(&values[..100_000]);
let mut right = build_mut_digest(&values[100_000..]);
- black_box(left.rank(0.5));
- black_box(right.rank(0.5));
+ black_box(left.rank(0.5).unwrap());
+ black_box(right.rank(0.5).unwrap());
bencher
.counter(ItemsCount::new(values.len()))
@@ -65,7 +65,7 @@ fn small_partials(bencher: Bencher) {
let partials = partial_digests(64, SMALL_ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
- black_box(digest.quantile(0.5));
+ black_box(digest.quantile(0.5).unwrap());
digest
})
.collect::<Vec<_>>();
@@ -86,7 +86,7 @@ fn partials(bencher: Bencher) {
let partials = partial_digests_with(DEFAULT_DIGEST_K, 64, ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
- black_box(digest.quantile(0.5));
+ black_box(digest.quantile(0.5).unwrap());
digest
})
.collect::<Vec<_>>();
@@ -107,7 +107,7 @@ fn partials_from_iter(bencher: Bencher) {
let partials = partial_digests_with(DEFAULT_DIGEST_K, 64, ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
- black_box(digest.quantile(0.5));
+ black_box(digest.quantile(0.5).unwrap());
digest
})
.collect::<Vec<_>>();
@@ -125,7 +125,7 @@ fn partials_from_iter_cloned(bencher: Bencher) {
let partials = partial_digests_with(DEFAULT_DIGEST_K, 64, ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
- black_box(digest.quantile(0.5));
+ black_box(digest.quantile(0.5).unwrap());
digest
})
.collect::<Vec<_>>();
@@ -141,7 +141,7 @@ fn partials_from_iter_finalize(bencher: Bencher,
additional_rows: usize) {
let partials = partial_digests_with(DEFAULT_DIGEST_K, 64, ROWS_PER_PARTIAL)
.into_iter()
.map(|mut digest| {
- black_box(digest.quantile(0.5));
+ black_box(digest.quantile(0.5).unwrap());
digest
})
.collect::<Vec<_>>();
@@ -188,7 +188,7 @@ fn serialized_partials(bencher: Bencher, rows_per_partial:
usize) {
let partial = TDigestMut::deserialize(partial).unwrap();
merged.merge(&partial);
}
- black_box(merged.quantile(0.5))
+ black_box(merged.quantile(0.5).unwrap())
});
}
@@ -208,7 +208,7 @@ fn serialized_overlapping_partials(bencher: Bencher) {
let partial = TDigestMut::deserialize(partial).unwrap();
merged.merge(&partial);
}
- black_box(merged.quantile(0.5))
+ black_box(merged.quantile(0.5).unwrap())
});
}
@@ -224,7 +224,7 @@ fn serialized_partials_from_iter(bencher: Bencher,
overlapping: bool) {
.map(|partial| TDigestMut::deserialize(partial))
.collect::<Result<TDigestMut, _>>()
.unwrap();
- black_box(merged.quantile(0.5))
+ black_box(merged.quantile(0.5).unwrap())
});
}
@@ -244,7 +244,7 @@ fn serialized_partials_in_batches(bencher: Bencher,
overlapping: bool) {
.unwrap();
merged.merge(&batch);
}
- black_box(merged.quantile(0.5))
+ black_box(merged.quantile(0.5).unwrap())
});
}
diff --git a/benchmarks/tdigest/query.rs b/benchmarks/tdigest/query.rs
index 338179e..76b05ac 100644
--- a/benchmarks/tdigest/query.rs
+++ b/benchmarks/tdigest/query.rs
@@ -29,7 +29,7 @@ use super::support::prepared_digest;
fn rank(bencher: Bencher) {
let digest = prepared_digest();
- bencher.bench_local(|| black_box(&digest).rank(black_box(0.531_25)));
+ bencher.bench_local(||
black_box(&digest).rank(black_box(0.531_25)).unwrap());
}
#[divan::bench(args = [0.0, 3.0, 3.5, 7.0])]
@@ -37,14 +37,14 @@ fn rank_repeated_means(bencher: Bencher, value: f64) {
let values = (0..100_000).map(|i| (i % 8) as f64).collect::<Vec<_>>();
let digest = build_digest(&values);
- bencher.bench_local(|| black_box(&digest).rank(black_box(value)));
+ bencher.bench_local(|| black_box(&digest).rank(black_box(value)).unwrap());
}
#[divan::bench]
fn quantile(bencher: Bencher) {
let digest = prepared_digest();
- bencher.bench_local(|| black_box(&digest).quantile(black_box(0.531_25)));
+ bencher.bench_local(||
black_box(&digest).quantile(black_box(0.531_25)).unwrap());
}
#[divan::bench]
@@ -54,7 +54,7 @@ fn cdf_100(bencher: Bencher) {
bencher
.counter(ItemsCount::new(split_points.len()))
- .bench_local(|| black_box(&digest).cdf(black_box(&split_points)));
+ .bench_local(||
black_box(&digest).cdf(black_box(&split_points)).unwrap());
}
#[divan::bench(consts = [true, false], args = [2, 6, 100])]
@@ -64,7 +64,7 @@ fn quantiles_sequential<const SORTED: bool>(bencher: Bencher,
num_ranks: usize)
bencher.counter(ItemsCount::new(num_ranks)).bench_local(|| {
for &rank in black_box(&ranks) {
- black_box(black_box(&digest).quantile(black_box(rank)));
+ black_box(black_box(&digest).quantile(black_box(rank)).unwrap());
}
});
}
@@ -76,7 +76,7 @@ fn quantiles_batch<const SORTED: bool>(bencher: Bencher,
num_ranks: usize) {
bencher
.counter(ItemsCount::new(num_ranks))
- .bench_local(|| black_box(&digest).quantiles(black_box(&ranks)));
+ .bench_local(||
black_box(&digest).quantiles(black_box(&ranks)).unwrap());
}
fn query_ranks(num_ranks: usize, sorted: bool) -> Vec<f64> {
diff --git a/datasketches/src/kll/mod.rs b/datasketches/src/kll/mod.rs
index 469bc7f..59e0fd6 100644
--- a/datasketches/src/kll/mod.rs
+++ b/datasketches/src/kll/mod.rs
@@ -31,13 +31,17 @@
//! # Usage
//!
//! ```rust
-//! # use datasketches::common::SearchCriteria;
-//! # use datasketches::kll::KllSketch;
-//! let mut sketch = KllSketch::<i64>::new(200).unwrap();
+//! use datasketches::common::SearchCriteria;
+//! use datasketches::kll::KllSketch;
+//!
+//! let mut sketch = KllSketch::<i64>::new(200)?;
//! sketch.update(1);
//! sketch.update(2);
-//! let q = sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap();
+//! let Some(q) = sketch.quantile(0.5, SearchCriteria::Inclusive)? else {
+//! return Ok(()); // No samples to query.
+//! };
//! assert!((1..=2).contains(&q));
+//! # Ok::<(), datasketches::error::Error>(())
//! ```
mod capacity;
diff --git a/datasketches/src/kll/sketch.rs b/datasketches/src/kll/sketch.rs
index 9fb3a62..891c4bb 100644
--- a/datasketches/src/kll/sketch.rs
+++ b/datasketches/src/kll/sketch.rs
@@ -79,7 +79,8 @@ impl<T: Clone + Ord> KllSketch<T> {
/// # Examples
///
/// ```
- /// # use datasketches::kll::KllSketch;
+ /// use datasketches::kll::KllSketch;
+ ///
/// let sketch = KllSketch::<i64>::new(200).unwrap();
/// assert_eq!(sketch.k(), 200);
/// ```
@@ -123,11 +124,15 @@ impl<T: Clone + Ord> KllSketch<T> {
}
/// Returns the minimum item seen by the sketch.
+ ///
+ /// Returns `None` if the sketch is empty.
pub fn min_item(&self) -> Option<&T> {
self.min_item.as_ref()
}
/// Returns the maximum item seen by the sketch.
+ ///
+ /// Returns `None` if the sketch is empty.
pub fn max_item(&self) -> Option<&T> {
self.max_item.as_ref()
}
@@ -202,12 +207,10 @@ impl<T: Clone + Ord> KllSketch<T> {
/// Returns the normalized rank of the given item.
///
- /// # Errors
- ///
- /// Returns an error if the sketch is empty.
- pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
+ /// Returns `None` if the sketch is empty.
+ pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty
sketch"));
+ return None;
}
let inclusive = criteria == SearchCriteria::Inclusive;
let mut weight = 0u64;
@@ -222,70 +225,87 @@ impl<T: Clone + Ord> KllSketch<T> {
.count() as u64;
weight += count << level;
}
- Ok(weight as f64 / self.n as f64)
+ Some(weight as f64 / self.n as f64)
}
/// Returns the quantile for the given normalized rank.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the sketch is empty.
///
/// # Errors
///
- /// Returns an error if the sketch is empty or `rank` is outside `[0.0,
1.0]`.
- pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T,
Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty
sketch"));
- }
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if the sketch is
+ /// empty.
+ pub fn quantile(&self, rank: f64, criteria: SearchCriteria) ->
Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
+ if self.is_empty() {
+ return Ok(None);
+ }
if rank == 0.0 {
- return Ok(self.min_item.as_ref().unwrap().clone());
+ return Ok(Some(self.min_item.as_ref().unwrap().clone()));
}
if rank == 1.0 {
- return Ok(self.max_item.as_ref().unwrap().clone());
+ return Ok(Some(self.max_item.as_ref().unwrap().clone()));
}
self.sorted_view().quantile(rank, criteria)
}
/// Returns approximate quantiles for the given normalized ranks.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the sketch is empty. Otherwise, an empty `ranks` slice
returns
+ /// `Ok(Some(vec![]))`.
///
/// The sorted view is built once for the whole batch.
///
/// # Errors
///
- /// Returns an error if the sketch is empty or any rank is outside `[0.0,
1.0]`.
- pub fn quantiles(&self, ranks: &[f64], criteria: SearchCriteria) ->
Result<Vec<T>, Error> {
+ /// Returns `InvalidArgument` if any rank is NaN or outside `[0.0, 1.0]`,
even if the sketch is
+ /// empty.
+ pub fn quantiles(
+ &self,
+ ranks: &[f64],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<T>>, Error> {
self.sorted_view().quantiles(ranks, criteria)
}
/// Returns the approximate CDF for the given split points.
///
+ /// Returns `Ok(None)` if the sketch is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the sketch is empty or the split points are not
unique and strictly
- /// increasing.
- pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty
sketch"));
- }
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the
+ /// sketch is empty.
+ pub fn cdf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().cdf(split_points, criteria)
}
/// Returns the approximate PMF for the given split points.
///
+ /// Returns `Ok(None)` if the sketch is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the sketch is empty or the split points are not
unique and strictly
- /// increasing.
- pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty
sketch"));
- }
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the
+ /// sketch is empty.
+ pub fn pmf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().pmf(split_points, criteria)
}
diff --git a/datasketches/src/kll/sorted_view.rs
b/datasketches/src/kll/sorted_view.rs
index d11f45b..35eba5d 100644
--- a/datasketches/src/kll/sorted_view.rs
+++ b/datasketches/src/kll/sorted_view.rs
@@ -70,12 +70,10 @@ impl<T: Clone + Ord> SortedView<T> {
/// Returns the approximate normalized rank of `item`.
///
- /// # Errors
- ///
- /// Returns an error if the view is empty.
- pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
+ /// Returns `None` if the view is empty.
+ pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty view"));
+ return None;
}
let index = if criteria == SearchCriteria::Inclusive {
upper_bound(&self.entries, item)
@@ -84,34 +82,36 @@ impl<T: Clone + Ord> SortedView<T> {
};
if index == 0 {
- return Ok(0.0);
+ return Some(0.0);
}
- Ok(self.entries[index - 1].cumulative_weight as f64 /
self.total_weight as f64)
+ Some(self.entries[index - 1].cumulative_weight as f64 /
self.total_weight as f64)
}
/// Returns the approximate quantile for `rank`.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the view is empty.
///
/// # Errors
///
- /// Returns an error if the view is empty or `rank` is outside `[0.0,
1.0]`.
- pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T,
Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty view"));
- }
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if the view is
+ /// empty.
+ pub fn quantile(&self, rank: f64, criteria: SearchCriteria) ->
Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank must be in [0.0, 1.0], got {rank}"
)));
}
+ if self.is_empty() {
+ return Ok(None);
+ }
// Compaction may discard extrema, and large weights can lose
precision in f64.
if rank == 0.0 {
- return Ok(self.min_item.as_ref().unwrap().clone());
+ return Ok(Some(self.min_item.as_ref().unwrap().clone()));
}
if rank == 1.0 {
- return Ok(self.max_item.as_ref().unwrap().clone());
+ return Ok(Some(self.max_item.as_ref().unwrap().clone()));
}
let weight = if criteria == SearchCriteria::Inclusive {
@@ -125,55 +125,93 @@ impl<T: Clone + Ord> SortedView<T> {
upper_bound_by_weight(&self.entries, weight)
};
- Ok(self.entries[index.min(self.entries.len() - 1)].item.clone())
+ Ok(Some(
+ self.entries[index.min(self.entries.len() - 1)].item.clone(),
+ ))
}
/// Returns approximate quantiles for all `ranks`.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the view is empty. Otherwise, an empty `ranks` slice
returns
+ /// `Ok(Some(vec![]))`.
///
/// # Errors
///
- /// Returns an error if the view is empty or any rank is outside `[0.0,
1.0]`.
- pub fn quantiles(&self, ranks: &[f64], criteria: SearchCriteria) ->
Result<Vec<T>, Error> {
+ /// Returns `InvalidArgument` if any rank is NaN or outside `[0.0, 1.0]`,
even if the view is
+ /// empty.
+ pub fn quantiles(
+ &self,
+ ranks: &[f64],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<T>>, Error> {
+ for &rank in ranks {
+ if !(0.0..=1.0).contains(&rank) {
+ return Err(Error::invalid_argument(format!(
+ "rank must be in [0.0, 1.0], got {rank}"
+ )));
+ }
+ }
if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty view"));
+ return Ok(None);
}
- ranks
+ let quantiles = ranks
.iter()
- .map(|&rank| self.quantile(rank, criteria))
- .collect()
+ .map(|&rank| {
+ self.quantile(rank, criteria)
+ .map(|quantile| quantile.expect("checked non-empty view"))
+ })
+ .collect::<Result<_, _>>()?;
+ Ok(Some(quantiles))
}
/// Returns the approximate cumulative distribution over `split_points`.
///
+ /// Returns `Ok(None)` if the view is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the view is empty or the split points are invalid.
- pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the view
+ /// is empty.
+ pub fn cdf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
+ check_split_points(split_points)?;
if self.is_empty() {
- return Err(Error::invalid_argument("cannot query an empty view"));
+ return Ok(None);
}
- check_split_points(split_points)?;
let mut ranks = Vec::with_capacity(split_points.len() + 1);
for item in split_points {
- ranks.push(self.rank(item, criteria)?);
+ ranks.push(self.rank(item, criteria).expect("checked non-empty
view"));
}
ranks.push(1.0);
- Ok(ranks)
+ Ok(Some(ranks))
}
/// Returns the approximate probability mass over `split_points`.
///
+ /// Returns `Ok(None)` if the view is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the view is empty or the split points are invalid.
- pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- let mut buckets = self.cdf(split_points, criteria)?;
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the view
+ /// is empty.
+ pub fn pmf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
+ let Some(mut buckets) = self.cdf(split_points, criteria)? else {
+ return Ok(None);
+ };
for index in (1..buckets.len()).rev() {
buckets[index] -= buckets[index - 1];
}
- Ok(buckets)
+ Ok(Some(buckets))
}
}
diff --git a/datasketches/src/req/mod.rs b/datasketches/src/req/mod.rs
index ce5a65f..40b787c 100644
--- a/datasketches/src/req/mod.rs
+++ b/datasketches/src/req/mod.rs
@@ -44,7 +44,9 @@
//! sketch.update(ReqFloat::<f64>::new(value)?);
//! }
//!
-//! let median = sketch.quantile(0.5, SearchCriteria::Inclusive)?;
+//! let Some(median) = sketch.quantile(0.5, SearchCriteria::Inclusive)? else {
+//! return Ok(()); // No samples to query.
+//! };
//! assert_eq!(median.into_inner(), 2.0);
//! # Ok::<(), datasketches::error::Error>(())
//! ```
diff --git a/datasketches/src/req/sketch.rs b/datasketches/src/req/sketch.rs
index 77a3284..446906f 100644
--- a/datasketches/src/req/sketch.rs
+++ b/datasketches/src/req/sketch.rs
@@ -176,16 +176,13 @@ where
/// Returns the approximate rank of `item` in `[0.0, 1.0]`.
///
- /// Computed directly from the retained items in a single `O(retained)`
pass,
- /// without building a sorted view. The result is identical to
- /// [`SortedView::rank`] on [`Self::sorted_view`].
+ /// Computed directly from the retained items in a single `O(retained)`
pass, without building
+ /// a sorted view. The result is identical to [`SortedView::rank`] on
[`Self::sorted_view`].
///
- /// # Errors
- ///
- /// Returns an error if the sketch is empty.
- pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
+ /// Returns `None` if the sketch is empty.
+ pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
+ return None;
}
let inclusive = matches!(criteria, SearchCriteria::Inclusive);
let weight: u64 = self
@@ -193,50 +190,56 @@ where
.iter()
.map(|c| c.count_below(item, inclusive) as u64 * c.weight())
.sum();
- Ok(weight as f64 / self.n as f64)
+ Some(weight as f64 / self.n as f64)
}
/// Returns the approximate quantile at the given normalized rank.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the sketch is empty.
///
/// For ranks strictly between `0.0` and `1.0`, builds a transient
[`SortedView`].
/// For repeated queries at those ranks, take one snapshot with
[`Self::sorted_view`].
///
/// # Errors
///
- /// Returns an error if the sketch is empty or `rank` is outside `[0.0,
1.0]`.
- pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T,
Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
- }
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if the sketch is
+ /// empty.
+ pub fn quantile(&self, rank: f64, criteria: SearchCriteria) ->
Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank {rank} must be in [0, 1]"
)));
}
+ if self.is_empty() {
+ return Ok(None);
+ }
if rank == 0.0 {
- return Ok(self.min_item.as_ref().unwrap().clone());
+ return Ok(Some(self.min_item.as_ref().unwrap().clone()));
}
if rank == 1.0 {
- return Ok(self.max_item.as_ref().unwrap().clone());
+ return Ok(Some(self.max_item.as_ref().unwrap().clone()));
}
self.sorted_view().quantile(rank, criteria)
}
/// Returns approximate quantiles for the given normalized ranks.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the sketch is empty. Otherwise, an empty `ranks` slice
returns
+ /// `Ok(Some(vec![]))`.
///
/// The sorted view is built once and shared across all ranks.
///
/// # Errors
///
- /// Returns an error if the sketch is empty or any rank is outside `[0.0,
1.0]`.
- pub fn quantiles(&self, ranks: &[f64], criteria: SearchCriteria) ->
Result<Vec<T>, Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
- }
+ /// Returns `InvalidArgument` if any rank is NaN or outside `[0.0, 1.0]`,
even if the sketch is
+ /// empty.
+ pub fn quantiles(
+ &self,
+ ranks: &[f64],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<T>>, Error> {
// Reject invalid ranks before paying for the view build.
for &r in ranks {
if !(0.0..=1.0).contains(&r) {
@@ -245,45 +248,63 @@ where
)));
}
}
+ if self.is_empty() {
+ return Ok(None);
+ }
let view = self.sorted_view();
- ranks.iter().map(|&r| view.quantile(r, criteria)).collect()
+ let quantiles = ranks
+ .iter()
+ .map(|&r| {
+ view.quantile(r, criteria)
+ .map(|quantile| quantile.expect("checked non-empty view"))
+ })
+ .collect::<Result<_, _>>()?;
+ Ok(Some(quantiles))
}
/// Returns the Probability Mass Function over the given split points.
///
+ /// Returns `Ok(None)` if the sketch is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the sketch is empty or the split points are not
strictly increasing.
- pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
- }
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the
+ /// sketch is empty.
+ pub fn pmf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().pmf(split_points, criteria)
}
/// Returns the Cumulative Distribution Function over the given split
points.
///
+ /// Returns `Ok(None)` if the sketch is empty. Otherwise, an empty
`split_points` slice returns
+ /// `Ok(Some(vec![1.0]))`.
+ ///
/// # Errors
///
- /// Returns an error if the sketch is empty or the split points are not
strictly increasing.
- pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
- }
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the
+ /// sketch is empty.
+ pub fn cdf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
self.sorted_view().cdf(split_points, criteria)
}
/// Returns an owned, sorted snapshot of the sketch's current state.
///
- /// An empty sketch yields an empty view; queries on it return an error.
The
- /// view is independent of the sketch — it can be queried (and sent to
other
- /// threads) while the sketch keeps receiving updates, and it keeps
answering
- /// from the state it was taken at.
+ /// An empty sketch yields an empty view. The view can be queried or sent
to other threads
+ /// while the sketch receives updates, preserving the state captured when
it was created.
///
/// Building the view costs `O(retained · log retained)`; rank and
quantile queries
/// then take `O(log retained)`. Prefer reusing one view for repeated
quantile
- /// queries at ranks strictly between `0.0` and `1.0`, or repeated
- /// [`Self::pmf`]/[`Self::cdf`] queries.
+ /// queries at ranks strictly between `0.0` and `1.0`, or repeated
[`Self::pmf`]/[`Self::cdf`]
+ /// queries.
pub fn sorted_view(&self) -> SortedView<T> {
let mut weighted_items = Vec::with_capacity(self.num_retained as
usize);
for compactor in &self.compactors {
@@ -371,6 +392,8 @@ where
}
/// Returns the lower bound for the normalized `rank` at the requested
confidence level.
+ ///
+ /// In exact mode, including an empty sketch, the bound equals `rank`.
pub fn rank_lower_bound(&self, rank: f64, num_std_dev: NumStdDev) -> f64 {
self.compute_rank_lower_bound(
self.k,
@@ -383,6 +406,8 @@ where
}
/// Returns the upper bound for the normalized `rank` at the requested
confidence level.
+ ///
+ /// In exact mode, including an empty sketch, the bound equals `rank`.
pub fn rank_upper_bound(&self, rank: f64, num_std_dev: NumStdDev) -> f64 {
self.compute_rank_upper_bound(
self.k,
diff --git a/datasketches/src/req/sorted_view.rs
b/datasketches/src/req/sorted_view.rs
index ec62f87..78c4bc6 100644
--- a/datasketches/src/req/sorted_view.rs
+++ b/datasketches/src/req/sorted_view.rs
@@ -22,12 +22,11 @@ use crate::error::Error;
/// An owned, sorted snapshot of a [`ReqSketch`](crate::req::ReqSketch).
///
-/// Obtain one with
[`ReqSketch::sorted_view`](crate::req::ReqSketch::sorted_view).
-/// The view is independent of the sketch: it can be queried (and sent to other
-/// threads) while the sketch keeps receiving updates, and it keeps answering
-/// from the state it was taken at. Building it costs `O(retained · log
retained)`;
-/// each subsequent query is `O(log retained)`, so it is the right tool for
-/// repeated quantile/rank queries.
+/// Obtain one with
[`ReqSketch::sorted_view`](crate::req::ReqSketch::sorted_view). The view is
+/// independent of the sketch: it can be queried (and sent to other threads)
while the sketch keeps
+/// receiving updates, and it keeps answering from the state it was taken at.
Building it costs
+/// `O(retained · log retained)`; each subsequent query is `O(log retained)`,
so it is the right
+/// tool for repeated quantile/rank queries.
#[derive(Debug, Clone)]
pub struct SortedView<T> {
items: Vec<T>,
@@ -93,48 +92,47 @@ where
/// Returns the approximate normalized rank of `item` in `[0.0, 1.0]`.
///
- /// # Errors
- ///
- /// Returns an error if the view is empty.
- pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
+ /// Returns `None` if the view is empty.
+ pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Option<f64> {
if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
+ return None;
}
let pos = match criteria {
SearchCriteria::Inclusive => self.items.partition_point(|x| x <=
item),
SearchCriteria::Exclusive => self.items.partition_point(|x| x <
item),
};
if pos == 0 {
- Ok(0.0)
+ Some(0.0)
} else {
- Ok(self.cumulative_weights[pos - 1] as f64 / self.total_weight as
f64)
+ Some(self.cumulative_weights[pos - 1] as f64 / self.total_weight
as f64)
}
}
/// Returns the approximate quantile at the given normalized rank.
///
- /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream.
+ /// Ranks `0.0` and `1.0` return the exact minimum and maximum items of
the stream. Returns
+ /// `Ok(None)` if the view is empty.
///
/// # Errors
///
- /// Returns an error if the view is empty or `rank` is outside `[0.0,
1.0]`.
- pub fn quantile(&self, rank: f64, criteria: SearchCriteria) -> Result<T,
Error> {
- if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
- }
-
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if the view is
+ /// empty.
+ pub fn quantile(&self, rank: f64, criteria: SearchCriteria) ->
Result<Option<T>, Error> {
if !(0.0..=1.0).contains(&rank) {
return Err(Error::invalid_argument(format!(
"rank {rank} must be in [0, 1]"
)));
}
+ if self.is_empty() {
+ return Ok(None);
+ }
// Compaction may discard extrema, and large weights can lose
precision in f64.
if rank == 0.0 {
- return Ok(self.min_item.as_ref().unwrap().clone());
+ return Ok(Some(self.min_item.as_ref().unwrap().clone()));
}
if rank == 1.0 {
- return Ok(self.max_item.as_ref().unwrap().clone());
+ return Ok(Some(self.max_item.as_ref().unwrap().clone()));
}
let target_weight = match criteria {
@@ -151,44 +149,61 @@ where
.partition_point(|&w| w <= target_weight),
};
- Ok(self.items[index.min(self.items.len() - 1)].clone())
+ Ok(Some(self.items[index.min(self.items.len() - 1)].clone()))
}
/// Returns the probability mass function (PMF) over the given split
points.
///
- /// The result contains one more value than `split_points`.
+ /// The result contains one more value than `split_points`. Returns
`Ok(None)` if the view is
+ /// empty. Otherwise, an empty `split_points` slice returns
`Ok(Some(vec![1.0]))`.
///
/// # Errors
///
- /// Returns an error if the view is empty or the split points are not
strictly increasing.
- pub fn pmf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
- let mut result = self.cdf(split_points, criteria)?;
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the view
+ /// is empty.
+ pub fn pmf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
+ let Some(mut result) = self.cdf(split_points, criteria)? else {
+ return Ok(None);
+ };
for index in (1..result.len()).rev() {
result[index] -= result[index - 1];
}
- Ok(result)
+ Ok(Some(result))
}
/// Returns the cumulative distribution function (CDF) over the given
split points.
///
- /// The result contains one more value than `split_points` and ends at
`1.0`.
+ /// The result contains one more value than `split_points` and ends at
`1.0`. Returns
+ /// `Ok(None)` if the view is empty. Otherwise, an empty `split_points`
slice returns
+ /// `Ok(Some(vec![1.0]))`.
///
/// # Errors
///
- /// Returns an error if the view is empty or the split points are not
strictly increasing.
- pub fn cdf(&self, split_points: &[T], criteria: SearchCriteria) ->
Result<Vec<f64>, Error> {
+ /// Returns `InvalidArgument` if the split points are not strictly
increasing, even if the view
+ /// is empty.
+ pub fn cdf(
+ &self,
+ split_points: &[T],
+ criteria: SearchCriteria,
+ ) -> Result<Option<Vec<f64>>, Error> {
+ self.validate_split_points(split_points)?;
if self.is_empty() {
- return Err(Error::invalid_argument("sketch is empty"));
+ return Ok(None);
}
- self.validate_split_points(split_points)?;
-
let mut result = Vec::with_capacity(split_points.len() + 1);
for split_point in split_points {
- result.push(self.rank(split_point, criteria)?);
+ result.push(
+ self.rank(split_point, criteria)
+ .expect("checked non-empty view"),
+ );
}
result.push(1.0);
- Ok(result)
+ Ok(Some(result))
}
fn validate_split_points(&self, split_points: &[T]) -> Result<(), Error> {
@@ -205,8 +220,6 @@ where
mod tests {
use googletest::assert_that;
use googletest::prelude::all;
- use googletest::prelude::anything;
- use googletest::prelude::err;
use googletest::prelude::ge;
use googletest::prelude::le;
use googletest::prelude::near;
@@ -227,21 +240,38 @@ mod tests {
}
#[test]
- fn test_rank_queries() -> Result<(), Error> {
+ fn test_rank_queries() {
let view = create_test_view();
// Test exact matches
- assert_that!(view.rank(&1, SearchCriteria::Inclusive)?, near(0.2,
1e-10));
- assert_that!(view.rank(&1, SearchCriteria::Exclusive)?, near(0.0,
1e-10));
+ assert_that!(
+ view.rank(&1, SearchCriteria::Inclusive).unwrap(),
+ near(0.2, 1e-10)
+ );
+ assert_that!(
+ view.rank(&1, SearchCriteria::Exclusive).unwrap(),
+ near(0.0, 1e-10)
+ );
// Test values between items
- assert_that!(view.rank(&2, SearchCriteria::Inclusive)?, near(0.2,
1e-10));
- assert_that!(view.rank(&6, SearchCriteria::Inclusive)?, near(0.6,
1e-10));
+ assert_that!(
+ view.rank(&2, SearchCriteria::Inclusive).unwrap(),
+ near(0.2, 1e-10)
+ );
+ assert_that!(
+ view.rank(&6, SearchCriteria::Inclusive).unwrap(),
+ near(0.6, 1e-10)
+ );
// Test edge cases
- assert_that!(view.rank(&0, SearchCriteria::Inclusive)?, near(0.0,
1e-10));
- assert_that!(view.rank(&10, SearchCriteria::Inclusive)?, near(1.0,
1e-10));
- Ok(())
+ assert_that!(
+ view.rank(&0, SearchCriteria::Inclusive).unwrap(),
+ near(0.0, 1e-10)
+ );
+ assert_that!(
+ view.rank(&10, SearchCriteria::Inclusive).unwrap(),
+ near(1.0, 1e-10)
+ );
}
#[test]
@@ -249,16 +279,16 @@ mod tests {
let view = create_test_view();
// Test edge cases
- assert_eq!(view.quantile(0.0, SearchCriteria::Inclusive)?, 1);
- assert_eq!(view.quantile(1.0, SearchCriteria::Inclusive)?, 9);
+ assert_eq!(view.quantile(0.0, SearchCriteria::Inclusive)?, Some(1));
+ assert_eq!(view.quantile(1.0, SearchCriteria::Inclusive)?, Some(9));
// Test middle values
- let median = view.quantile(0.5, SearchCriteria::Inclusive)?;
+ let median = view.quantile(0.5, SearchCriteria::Inclusive)?.unwrap();
assert_that!(median, all!(ge(3), le(7))); // Should be around the
middle (values are 1,3,5,7,9)
// Test various ranks
- let q25 = view.quantile(0.25, SearchCriteria::Inclusive)?;
- let q75 = view.quantile(0.75, SearchCriteria::Inclusive)?;
+ let q25 = view.quantile(0.25, SearchCriteria::Inclusive)?.unwrap();
+ let q75 = view.quantile(0.75, SearchCriteria::Inclusive)?.unwrap();
assert_that!(q25, le(median));
assert_that!(median, le(q75));
Ok(())
@@ -269,7 +299,7 @@ mod tests {
let view = create_test_view();
let split_points = vec![3, 7];
- let pmf = view.pmf(&split_points, SearchCriteria::Inclusive)?;
+ let pmf = view.pmf(&split_points, SearchCriteria::Inclusive)?.unwrap();
assert_eq!(pmf.len(), 3); // 2 split points create 3 intervals
// Sum should be approximately 1.0
@@ -283,7 +313,7 @@ mod tests {
let view = create_test_view();
let split_points = vec![3, 7];
- let cdf = view.cdf(&split_points, SearchCriteria::Inclusive)?;
+ let cdf = view.cdf(&split_points, SearchCriteria::Inclusive)?.unwrap();
assert_eq!(cdf.len(), 3);
// CDF should be monotonically increasing
@@ -303,11 +333,7 @@ mod tests {
assert_eq!(view.len(), 0);
assert_eq!(view.total_weight(), 0);
- // Operations on empty view should return errors
- assert_that!(view.rank(&5, SearchCriteria::Inclusive),
err(anything()));
- assert_that!(
- view.quantile(0.5, SearchCriteria::Inclusive),
- err(anything())
- );
+ assert_eq!(view.rank(&5, SearchCriteria::Inclusive), None);
+ assert_eq!(view.quantile(0.5, SearchCriteria::Inclusive).unwrap(),
None);
}
}
diff --git a/datasketches/src/tdigest/mod.rs b/datasketches/src/tdigest/mod.rs
index 0d46618..8c5818b 100644
--- a/datasketches/src/tdigest/mod.rs
+++ b/datasketches/src/tdigest/mod.rs
@@ -56,9 +56,13 @@
//! let mut sketch = TDigestMut::new(100).unwrap();
//! sketch.update(1.0);
//! sketch.update(2.0);
-//! let median = sketch.quantile(0.5).unwrap();
+//! let Some(median) = sketch.quantile(0.5)? else {
+//! return Ok(()); // No samples to query.
+//! };
+//! assert!((1.0..=2.0).contains(&median));
//! let frozen = sketch.freeze();
-//! assert!(frozen.rank(2.0).is_some());
+//! assert!(frozen.rank(2.0)?.is_some());
+//! # Ok::<(), datasketches::error::Error>(())
//! ```
mod serialization;
diff --git a/datasketches/src/tdigest/sketch.rs
b/datasketches/src/tdigest/sketch.rs
index 810c86b..d7599af 100644
--- a/datasketches/src/tdigest/sketch.rs
+++ b/datasketches/src/tdigest/sketch.rs
@@ -403,10 +403,12 @@ impl TDigestMut {
/// Returns the cumulative distribution approximation described by
[`TDigest::cdf`].
///
- /// # Panics
+ /// Returns `Ok(None)` if this t-digest is empty.
+ ///
+ /// # Errors
///
- /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
- /// values.
+ /// Returns `InvalidArgument` if `split_points` contains NaN or is not
strictly increasing, even
+ /// if this t-digest is empty.
///
/// # Examples
///
@@ -417,25 +419,27 @@ impl TDigestMut {
/// for value in [1.0, 2.0, 3.0] {
/// sketch.update(value);
/// }
- /// let cdf = sketch.cdf(&[1.5]).unwrap();
+ /// let cdf = sketch.cdf(&[1.5]).unwrap().unwrap();
/// assert_eq!(cdf.len(), 2);
/// ```
- pub fn cdf(&mut self, split_points: &[f64]) -> Option<Vec<f64>> {
- check_split_points(split_points);
+ pub fn cdf(&mut self, split_points: &[f64]) -> Result<Option<Vec<f64>>,
Error> {
+ check_split_points(split_points)?;
if self.is_empty() {
- return None;
+ return Ok(None);
}
- self.view().cdf(split_points)
+ Ok(self.view().cdf(split_points))
}
/// Returns the probability mass approximation described by
[`TDigest::pmf`].
///
- /// # Panics
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
- /// values.
+ /// # Errors
+ ///
+ /// Returns `InvalidArgument` if `split_points` contains NaN or is not
strictly increasing, even
+ /// if this t-digest is empty.
///
/// # Examples
///
@@ -446,24 +450,26 @@ impl TDigestMut {
/// for value in [1.0, 2.0, 3.0] {
/// sketch.update(value);
/// }
- /// let pmf = sketch.pmf(&[1.5]).unwrap();
+ /// let pmf = sketch.pmf(&[1.5]).unwrap().unwrap();
/// assert_eq!(pmf.len(), 2);
/// ```
- pub fn pmf(&mut self, split_points: &[f64]) -> Option<Vec<f64>> {
- check_split_points(split_points);
+ pub fn pmf(&mut self, split_points: &[f64]) -> Result<Option<Vec<f64>>,
Error> {
+ check_split_points(split_points)?;
if self.is_empty() {
- return None;
+ return Ok(None);
}
- self.view().pmf(split_points)
+ Ok(self.view().pmf(split_points))
}
/// Returns the normalized rank described by [`TDigest::rank`].
///
- /// # Panics
+ /// Returns `Ok(None)` if this t-digest is empty.
+ ///
+ /// # Errors
///
- /// Panics if `value` is `NaN`.
+ /// Returns `InvalidArgument` if `value` is NaN, even if this t-digest is
empty.
///
/// # Examples
///
@@ -474,33 +480,38 @@ impl TDigestMut {
/// for value in [1.0, 2.0, 3.0] {
/// sketch.update(value);
/// }
- /// let rank = sketch.rank(2.0).unwrap();
+ /// let rank = sketch.rank(2.0).unwrap().unwrap();
/// assert!((0.0..=1.0).contains(&rank));
/// ```
- pub fn rank(&mut self, value: f64) -> Option<f64> {
- assert!(!value.is_nan(), "value must not be NaN");
+ pub fn rank(&mut self, value: f64) -> Result<Option<f64>, Error> {
+ if value.is_nan() {
+ return Err(Error::invalid_argument("value must not be NaN"));
+ }
if self.is_empty() {
- return None;
+ return Ok(None);
}
if value < self.min {
- return Some(0.0);
+ return Ok(Some(0.0));
}
if value > self.max {
- return Some(1.0);
+ return Ok(Some(1.0));
}
if self.min == self.max {
- return Some(0.5);
+ return Ok(Some(0.5));
}
- self.view().rank(value)
+ Ok(self.view().rank(value))
}
/// Returns the quantile described by [`TDigest::quantile`].
///
- /// # Panics
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// Panics if `rank` is outside `[0.0, 1.0]`.
+ /// # Errors
+ ///
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if this t-digest is
+ /// empty.
///
/// # Examples
///
@@ -511,35 +522,36 @@ impl TDigestMut {
/// for value in [1.0, 2.0, 3.0] {
/// sketch.update(value);
/// }
- /// let median = sketch.quantile(0.5).unwrap();
+ /// let median = sketch.quantile(0.5).unwrap().unwrap();
/// assert!((1.0..=3.0).contains(&median));
/// ```
- pub fn quantile(&mut self, rank: f64) -> Option<f64> {
- assert!(
- (0.0..=1.0).contains(&rank),
- "rank must be in [0.0, 1.0]; got {rank}"
- );
+ pub fn quantile(&mut self, rank: f64) -> Result<Option<f64>, Error> {
+ check_rank(rank)?;
if self.is_empty() {
- return None;
+ return Ok(None);
}
- self.view().quantile(rank)
+ Ok(self.view().quantile(rank))
}
/// Returns the quantiles described by [`TDigest::quantiles`].
///
- /// # Panics
+ /// Returns `Ok(None)` if this t-digest is empty. Otherwise, an empty
`ranks` slice returns
+ /// `Ok(Some(vec![]))`.
///
- /// Panics if any rank is outside `[0.0, 1.0]`.
- pub fn quantiles(&mut self, ranks: &[f64]) -> Option<Vec<f64>> {
- check_ranks(ranks);
+ /// # Errors
+ ///
+ /// Returns `InvalidArgument` if any rank is NaN or outside `[0.0, 1.0]`,
even if this t-digest
+ /// is empty.
+ pub fn quantiles(&mut self, ranks: &[f64]) -> Result<Option<Vec<f64>>,
Error> {
+ check_ranks(ranks)?;
if self.is_empty() {
- return None;
+ return Ok(None);
}
- self.view().quantiles(ranks)
+ Ok(self.view().quantiles(ranks))
}
/// Serializes this mutable t-digest to bytes.
@@ -1427,14 +1439,15 @@ impl TDigest {
/// stream given the split points. The value at array position j of the
returned CDF array
/// is the sum of the returned values in positions 0 through j of the
returned PMF array.
/// This can be viewed as array of ranks of the given split points plus
one more value that
- /// is always 1. An empty `split_points` slice returns the single value
`[1.0]`.
+ /// is always 1. For a non-empty t-digest, an empty `split_points` slice
returns
+ /// `Ok(Some(vec![1.0]))`.
///
- /// Returns `None` if this t-digest is empty.
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// # Panics
+ /// # Errors
///
- /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
- /// values.
+ /// Returns `InvalidArgument` if `split_points` contains NaN or is not
strictly increasing, even
+ /// if this t-digest is empty.
///
/// # Examples
///
@@ -1446,11 +1459,12 @@ impl TDigest {
/// sketch.update(value);
/// }
/// let digest = sketch.freeze();
- /// let cdf = digest.cdf(&[1.5]).unwrap();
+ /// let cdf = digest.cdf(&[1.5]).unwrap().unwrap();
/// assert_eq!(cdf.len(), 2);
/// ```
- pub fn cdf(&self, split_points: &[f64]) -> Option<Vec<f64>> {
- self.view().cdf(split_points)
+ pub fn cdf(&self, split_points: &[f64]) -> Result<Option<Vec<f64>>, Error>
{
+ check_split_points(split_points)?;
+ Ok(self.view().cdf(split_points))
}
/// Returns an approximation to the Probability Mass Function (PMF) of the
input stream
@@ -1465,14 +1479,14 @@ impl TDigest {
///
/// An array of m+1 doubles each of which is an approximation to the
fraction of the input
/// stream values (the mass) that fall into one of those intervals.
- /// An empty `split_points` slice returns the single value `[1.0]`.
+ /// For a non-empty t-digest, an empty `split_points` slice returns
`Ok(Some(vec![1.0]))`.
///
- /// Returns `None` if this t-digest is empty.
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// # Panics
+ /// # Errors
///
- /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
- /// values.
+ /// Returns `InvalidArgument` if `split_points` contains NaN or is not
strictly increasing, even
+ /// if this t-digest is empty.
///
/// # Examples
///
@@ -1484,20 +1498,21 @@ impl TDigest {
/// sketch.update(value);
/// }
/// let digest = sketch.freeze();
- /// let pmf = digest.pmf(&[1.5]).unwrap();
+ /// let pmf = digest.pmf(&[1.5]).unwrap().unwrap();
/// assert_eq!(pmf.len(), 2);
/// ```
- pub fn pmf(&self, split_points: &[f64]) -> Option<Vec<f64>> {
- self.view().pmf(split_points)
+ pub fn pmf(&self, split_points: &[f64]) -> Result<Option<Vec<f64>>, Error>
{
+ check_split_points(split_points)?;
+ Ok(self.view().pmf(split_points))
}
- /// Computes the approximate normalized rank in `[0.0, 1.0]` of the given
value.
+ /// Returns the approximate normalized rank in `[0.0, 1.0]` of the given
value.
///
- /// Returns `None` if this t-digest is empty.
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// # Panics
+ /// # Errors
///
- /// Panics if the value is `NaN`.
+ /// Returns `InvalidArgument` if `value` is NaN, even if this t-digest is
empty.
///
/// # Examples
///
@@ -1509,23 +1524,26 @@ impl TDigest {
/// sketch.update(value);
/// }
/// let digest = sketch.freeze();
- /// let rank = digest.rank(2.0).unwrap();
+ /// let rank = digest.rank(2.0).unwrap().unwrap();
/// assert!((0.0..=1.0).contains(&rank));
/// ```
- pub fn rank(&self, value: f64) -> Option<f64> {
- assert!(!value.is_nan(), "value must not be NaN");
- self.view().rank(value)
+ pub fn rank(&self, value: f64) -> Result<Option<f64>, Error> {
+ if value.is_nan() {
+ return Err(Error::invalid_argument("value must not be NaN"));
+ }
+ Ok(self.view().rank(value))
}
- /// Computes the approximate quantile for the given normalized rank.
+ /// Returns the approximate quantile for the given normalized rank.
///
/// Ranks `0.0` and `1.0` return the stored minimum and maximum,
respectively.
///
- /// Returns `None` if this t-digest is empty.
+ /// Returns `Ok(None)` if this t-digest is empty.
///
- /// # Panics
+ /// # Errors
///
- /// Panics if `rank` is outside `[0.0, 1.0]`.
+ /// Returns `InvalidArgument` if `rank` is NaN or outside `[0.0, 1.0]`,
even if this t-digest is
+ /// empty.
///
/// # Examples
///
@@ -1537,24 +1555,26 @@ impl TDigest {
/// sketch.update(value);
/// }
/// let digest = sketch.freeze();
- /// let q = digest.quantile(0.5).unwrap();
+ /// let q = digest.quantile(0.5).unwrap().unwrap();
/// assert!((1.0..=3.0).contains(&q));
/// ```
- pub fn quantile(&self, rank: f64) -> Option<f64> {
- assert!((0.0..=1.0).contains(&rank), "rank must be in [0.0, 1.0]");
- self.view().quantile(rank)
+ pub fn quantile(&self, rank: f64) -> Result<Option<f64>, Error> {
+ check_rank(rank)?;
+ Ok(self.view().quantile(rank))
}
- /// Computes approximate quantiles for the given normalized ranks.
+ /// Returns approximate quantiles for the given normalized ranks.
///
/// Ranks in nondecreasing order are answered with one centroid scan.
Ranks in any other order
/// are accepted and results are returned in the same order as the input.
///
- /// Returns `None` if this t-digest is empty.
+ /// Returns `Ok(None)` if this t-digest is empty. Otherwise, an empty
`ranks` slice returns
+ /// `Ok(Some(vec![]))`.
///
- /// # Panics
+ /// # Errors
///
- /// Panics if any rank is outside `[0.0, 1.0]`.
+ /// Returns `InvalidArgument` if any rank is NaN or outside `[0.0, 1.0]`,
even if this t-digest
+ /// is empty.
///
/// # Examples
///
@@ -1566,12 +1586,12 @@ impl TDigest {
/// sketch.update(value);
/// }
/// let digest = sketch.freeze();
- /// let quantiles = digest.quantiles(&[0.25, 0.5, 0.75]).unwrap();
+ /// let quantiles = digest.quantiles(&[0.25, 0.5, 0.75]).unwrap().unwrap();
/// assert_eq!(quantiles.len(), 3);
/// ```
- pub fn quantiles(&self, ranks: &[f64]) -> Option<Vec<f64>> {
- check_ranks(ranks);
- self.view().quantiles(ranks)
+ pub fn quantiles(&self, ranks: &[f64]) -> Result<Option<Vec<f64>>, Error> {
+ check_ranks(ranks)?;
+ Ok(self.view().quantiles(ranks))
}
/// Converts this immutable t-digest into a mutable one.
@@ -1622,8 +1642,6 @@ impl TDigestView<'_> {
}
fn cdf(&self, split_points: &[f64]) -> Option<Vec<f64>> {
- check_split_points(split_points);
-
if self.centroids.is_empty() {
return None;
}
@@ -1896,22 +1914,34 @@ impl TDigestView<'_> {
/// Checks the sequential validity of the given array of double values.
/// They must be unique, monotonically increasing and not NaN.
-#[track_caller]
-fn check_split_points(split_points: &[f64]) {
+fn check_split_points(split_points: &[f64]) -> Result<(), Error> {
if split_points.iter().any(|split_point| split_point.is_nan()) {
- panic!("split_points must not contain NaN values: {split_points:?}");
+ return Err(Error::invalid_argument(
+ "split_points must not contain NaN values",
+ ));
}
if !split_points.windows(2).all(|pair| pair[0] < pair[1]) {
- panic!("split_points must be unique and monotonically increasing:
{split_points:?}");
+ return Err(Error::invalid_argument(
+ "split_points must be unique and monotonically increasing",
+ ));
}
+ Ok(())
}
-#[track_caller]
-fn check_ranks(ranks: &[f64]) {
- assert!(
- ranks.iter().all(|rank| (0.0..=1.0).contains(rank)),
- "ranks must be in [0.0, 1.0]; got {ranks:?}"
- );
+fn check_rank(rank: f64) -> Result<(), Error> {
+ if !(0.0..=1.0).contains(&rank) {
+ return Err(Error::invalid_argument(format!(
+ "rank must be in [0.0, 1.0], got {rank}"
+ )));
+ }
+ Ok(())
+}
+
+fn check_ranks(ranks: &[f64]) -> Result<(), Error> {
+ for &rank in ranks {
+ check_rank(rank)?;
+ }
+ Ok(())
}
fn centroid_cmp(a: &Centroid, b: &Centroid) -> Ordering {
diff --git a/tests-integration/tests/kll_test/generic.rs
b/tests-integration/tests/kll_test/generic.rs
index 14d0667..544f679 100644
--- a/tests-integration/tests/kll_test/generic.rs
+++ b/tests-integration/tests/kll_test/generic.rs
@@ -68,7 +68,11 @@ fn
custom_item_order_controls_queries_and_survives_roundtrip() {
assert_eq!(sketch.min_item().map(|item| item.0.as_str()), Some("1"));
assert_eq!(sketch.max_item().map(|item| item.0.as_str()), Some("10"));
assert_eq!(
- sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap().0,
+ sketch
+ .quantile(0.5, SearchCriteria::Inclusive)
+ .unwrap()
+ .unwrap()
+ .0,
"2"
);
@@ -78,7 +82,11 @@ fn
custom_item_order_controls_queries_and_survives_roundtrip() {
assert_eq!(decoded.min_item().map(|item| item.0.as_str()), Some("1"));
assert_eq!(decoded.max_item().map(|item| item.0.as_str()), Some("10"));
assert_eq!(
- decoded.quantile(0.5, SearchCriteria::Inclusive).unwrap().0,
+ decoded
+ .quantile(0.5, SearchCriteria::Inclusive)
+ .unwrap()
+ .unwrap()
+ .0,
"2"
);
}
diff --git a/tests-integration/tests/kll_test/merge.rs
b/tests-integration/tests/kll_test/merge.rs
index 417fa1b..513f0db 100644
--- a/tests-integration/tests/kll_test/merge.rs
+++ b/tests-integration/tests/kll_test/merge.rs
@@ -35,6 +35,7 @@ fn merge_preserves_weight_extrema_and_query_invariants() {
assert_eq!(left.sorted_view().total_weight(), left.n());
let quantiles = left
.quantiles(&[0.0, 0.25, 0.5, 0.75, 1.0], SearchCriteria::Inclusive)
+ .unwrap()
.unwrap();
assert!(quantiles.windows(2).all(|pair| pair[0] <= pair[1]));
}
diff --git a/tests-integration/tests/kll_test/query.rs
b/tests-integration/tests/kll_test/query.rs
index 3730224..aa303e3 100644
--- a/tests-integration/tests/kll_test/query.rs
+++ b/tests-integration/tests/kll_test/query.rs
@@ -35,18 +35,24 @@ fn quantile_endpoints_preserve_extrema_after_compaction() {
let view = sketch.sorted_view();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
- assert_eq!(sketch.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(sketch.quantile(1.0, criteria).unwrap(), 7);
- assert_eq!(sketch.quantiles(&[0.0, 1.0], criteria).unwrap(), [0, 7]);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(), Some(7));
+ assert_eq!(
+ sketch.quantiles(&[0.0, 1.0], criteria).unwrap(),
+ Some(vec![0, 7])
+ );
}
sketch.reset();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
- assert!(sketch.quantile(0.0, criteria).is_err());
- assert!(sketch.quantile(1.0, criteria).is_err());
- assert_eq!(view.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(view.quantile(1.0, criteria).unwrap(), 7);
- assert_eq!(view.quantiles(&[0.0, 1.0], criteria).unwrap(), [0, 7]);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(), None);
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(), None);
+ assert_eq!(view.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(view.quantile(1.0, criteria).unwrap(), Some(7));
+ assert_eq!(
+ view.quantiles(&[0.0, 1.0], criteria).unwrap(),
+ Some(vec![0, 7])
+ );
}
}
@@ -62,30 +68,85 @@ fn quantile_endpoints_with_large_stream_weight() {
let view = sketch.sorted_view();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
- assert_eq!(sketch.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(sketch.quantile(1.0, criteria).unwrap(), 1);
- assert_eq!(view.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(view.quantile(1.0, criteria).unwrap(), 1);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(), Some(1));
+ assert_eq!(view.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(view.quantile(1.0, criteria).unwrap(), Some(1));
}
}
#[test]
-fn empty_and_invalid_queries_return_errors() {
- let mut sketch = KllSketch::<i64>::new(DEFAULT_K).unwrap();
- assert!(sketch.rank(&0, SearchCriteria::Inclusive).is_err());
- assert!(sketch.quantile(0.5, SearchCriteria::Inclusive).is_err());
- assert!(sketch.pmf(&[0], SearchCriteria::Inclusive).is_err());
- assert!(sketch.cdf(&[0], SearchCriteria::Inclusive).is_err());
+fn valid_queries_on_empty_sketch_return_no_result() {
+ let sketch = KllSketch::<i64>::new(DEFAULT_K).unwrap();
+ let view = sketch.sorted_view();
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
+ assert_eq!(sketch.rank(&0, criteria), None);
+ assert_eq!(view.rank(&0, criteria), None);
+ for rank in [0.0, 0.5, 1.0] {
+ assert_eq!(sketch.quantile(rank, criteria).unwrap(), None);
+ assert_eq!(view.quantile(rank, criteria).unwrap(), None);
+ }
+ assert_eq!(sketch.quantiles(&[0.0, 0.5, 1.0], criteria).unwrap(),
None);
+ assert_eq!(view.quantiles(&[0.0, 0.5, 1.0], criteria).unwrap(), None);
+ assert_eq!(sketch.cdf(&[0], criteria).unwrap(), None);
+ assert_eq!(sketch.pmf(&[0], criteria).unwrap(), None);
+ assert_eq!(view.cdf(&[0], criteria).unwrap(), None);
+ assert_eq!(view.pmf(&[0], criteria).unwrap(), None);
+ }
+}
- sketch.update(0);
- for rank in [-1.0, f64::NAN, 1.1] {
- let error = sketch
- .quantile(rank, SearchCriteria::Inclusive)
- .unwrap_err();
- assert_eq!(error.kind(), ErrorKind::InvalidArgument);
- }
- let error = sketch.cdf(&[1, 0], SearchCriteria::Inclusive).unwrap_err();
- assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+#[test]
+fn empty_argument_slices_preserve_the_sketch_empty_state() {
+ for n in [0, 1] {
+ let mut sketch = KllSketch::<i64>::new(DEFAULT_K).unwrap();
+ for item in 0..n {
+ sketch.update(item);
+ }
+ let view = sketch.sorted_view();
+ let quantiles = (n != 0).then(Vec::new);
+ let distribution = (n != 0).then(|| vec![1.0]);
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive]
{
+ assert_eq!(sketch.quantiles(&[], criteria).unwrap(), quantiles);
+ assert_eq!(view.quantiles(&[], criteria).unwrap(), quantiles);
+ assert_eq!(sketch.cdf(&[], criteria).unwrap(), distribution);
+ assert_eq!(sketch.pmf(&[], criteria).unwrap(), distribution);
+ assert_eq!(view.cdf(&[], criteria).unwrap(), distribution);
+ assert_eq!(view.pmf(&[], criteria).unwrap(), distribution);
+ }
+ }
+}
+
+#[test]
+fn invalid_arguments_are_rejected_regardless_of_sketch_state() {
+ for n in [0, 1, 1_000] {
+ let mut sketch = KllSketch::<i64>::new(DEFAULT_K).unwrap();
+ for item in 0..n {
+ sketch.update(item);
+ }
+ let view = sketch.sorted_view();
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive]
{
+ for rank in [f64::NAN, f64::NEG_INFINITY, -0.1, 1.1,
f64::INFINITY] {
+ for error in [
+ sketch.quantile(rank, criteria).unwrap_err(),
+ view.quantile(rank, criteria).unwrap_err(),
+ sketch.quantiles(&[0.5, rank], criteria).unwrap_err(),
+ view.quantiles(&[0.5, rank], criteria).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
+ }
+ for splits in [[1, 1], [2, 1]] {
+ for error in [
+ sketch.cdf(&splits, criteria).unwrap_err(),
+ sketch.pmf(&splits, criteria).unwrap_err(),
+ view.cdf(&splits, criteria).unwrap_err(),
+ view.pmf(&splits, criteria).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
+ }
+ }
+ }
}
#[test]
@@ -99,8 +160,14 @@ fn inclusive_and_exclusive_semantics_cover_duplicates() {
assert_eq!(sketch.rank(&1, SearchCriteria::Inclusive).unwrap(), 0.5);
assert_eq!(sketch.rank(&2, SearchCriteria::Exclusive).unwrap(), 0.5);
assert_eq!(sketch.rank(&2, SearchCriteria::Inclusive).unwrap(), 1.0);
- assert_eq!(sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap(), 1);
- assert_eq!(sketch.quantile(0.5, SearchCriteria::Exclusive).unwrap(), 2);
+ assert_eq!(
+ sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap(),
+ Some(1)
+ );
+ assert_eq!(
+ sketch.quantile(0.5, SearchCriteria::Exclusive).unwrap(),
+ Some(2)
+ );
}
#[test]
@@ -110,11 +177,17 @@ fn exact_mode_queries_match_the_stream() {
sketch.update(item);
}
- assert_eq!(sketch.quantile(0.0, SearchCriteria::Inclusive).unwrap(), 1);
- assert_eq!(sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap(), 50);
+ assert_eq!(
+ sketch.quantile(0.0, SearchCriteria::Inclusive).unwrap(),
+ Some(1)
+ );
+ assert_eq!(
+ sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap(),
+ Some(50)
+ );
assert_eq!(
sketch.quantile(1.0, SearchCriteria::Inclusive).unwrap(),
- 100
+ Some(100)
);
for item in 1..=100 {
assert_eq!(
@@ -152,8 +225,8 @@ fn rank_cdf_and_pmf_are_consistent() {
let split_points: Vec<_> = (100..10_000).step_by(100).collect();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
- let cdf = sketch.cdf(&split_points, criteria).unwrap();
- let pmf = sketch.pmf(&split_points, criteria).unwrap();
+ let cdf = sketch.cdf(&split_points, criteria).unwrap().unwrap();
+ let pmf = sketch.pmf(&split_points, criteria).unwrap().unwrap();
let mut subtotal = 0.0;
for (index, split_point) in split_points.iter().enumerate() {
subtotal += pmf[index];
@@ -172,18 +245,23 @@ fn sorted_view_supports_repeated_and_batch_queries() {
}
let view = sketch.sorted_view();
let ranks = [0.0, 0.25, 0.5, 0.75, 1.0];
- let quantiles = sketch.quantiles(&ranks,
SearchCriteria::Inclusive).unwrap();
+ let quantiles = sketch
+ .quantiles(&ranks, SearchCriteria::Inclusive)
+ .unwrap()
+ .unwrap();
assert_eq!(view.len(), sketch.num_retained());
assert_eq!(view.total_weight(), sketch.n());
assert_eq!(
- view.quantiles(&ranks, SearchCriteria::Inclusive).unwrap(),
+ view.quantiles(&ranks, SearchCriteria::Inclusive)
+ .unwrap()
+ .unwrap(),
quantiles
);
for (&rank, quantile) in ranks.iter().zip(&quantiles) {
assert_eq!(
view.quantile(rank, SearchCriteria::Inclusive).unwrap(),
- *quantile
+ Some(*quantile)
);
assert_eq!(
view.rank(quantile, SearchCriteria::Inclusive).unwrap(),
@@ -193,5 +271,8 @@ fn sorted_view_supports_repeated_and_batch_queries() {
sketch.update(2_000);
assert_eq!(view.total_weight(), 1_000);
- assert_eq!(view.quantile(1.0, SearchCriteria::Inclusive).unwrap(), 999);
+ assert_eq!(
+ view.quantile(1.0, SearchCriteria::Inclusive).unwrap(),
+ Some(999)
+ );
}
diff --git a/tests-integration/tests/req_test/accuracy.rs
b/tests-integration/tests/req_test/accuracy.rs
index 76c318c..0fc361c 100644
--- a/tests-integration/tests/req_test/accuracy.rs
+++ b/tests-integration/tests/req_test/accuracy.rs
@@ -38,8 +38,12 @@ fn rank_space_error_is_bounded() -> Result<(), Error> {
assert_eq!(sketch.n(), n as u64);
for rank in [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
- let quantile = sketch.quantile(rank, SearchCriteria::Inclusive)?;
- let estimated_rank = sketch.rank(&quantile,
SearchCriteria::Inclusive)?;
+ let quantile = sketch
+ .quantile(rank, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty");
+ let estimated_rank = sketch
+ .rank(&quantile, SearchCriteria::Inclusive)
+ .expect("the sketch is non-empty");
let abs_rank_error = (estimated_rank - rank).abs();
let max_abs_rank_error = if rank >= 0.9 { 0.01 } else { 0.02 };
diff --git a/tests-integration/tests/req_test/bounds.rs
b/tests-integration/tests/req_test/bounds.rs
index d6ee3a7..a0c6199 100644
--- a/tests-integration/tests/req_test/bounds.rs
+++ b/tests-integration/tests/req_test/bounds.rs
@@ -77,7 +77,9 @@ fn theoretical_error_bounds_cover_uniform_quantiles() ->
Result<(), Error> {
0.95, 0.97, 0.98, 0.99, 0.995, 0.999,
] {
let true_quantile = req_f64(rank * (n - 1) as f64);
- let estimated_rank = sketch.rank(&true_quantile,
SearchCriteria::Inclusive)?;
+ let estimated_rank = sketch
+ .rank(&true_quantile, SearchCriteria::Inclusive)
+ .expect("the sketch is non-empty");
let lower = sketch.rank_lower_bound(rank, NumStdDev::Three);
let upper = sketch.rank_upper_bound(rank, NumStdDev::Three);
assert_that!(estimated_rank, all!(ge(lower), le(upper)), "rank:
{rank}");
diff --git a/tests-integration/tests/req_test/core.rs
b/tests-integration/tests/req_test/core.rs
index 626632d..1a3622e 100644
--- a/tests-integration/tests/req_test/core.rs
+++ b/tests-integration/tests/req_test/core.rs
@@ -42,7 +42,7 @@ use super::req_f32;
use super::req_f64;
#[test]
-fn empty_sketch_has_default_state_and_rejects_queries() {
+fn empty_sketch_has_default_state_and_absent_queries() {
let sketch: ReqSketch<ReqF64> = ReqSketch::default();
assert_eq!(sketch.k(), 12);
@@ -53,22 +53,23 @@ fn empty_sketch_has_default_state_and_rejects_queries() {
assert_that!(sketch.min_item(), none());
assert_that!(sketch.max_item(), none());
- assert_that!(
- sketch.rank(&req_f64(0.0), SearchCriteria::Inclusive),
- err(anything())
- );
- assert_that!(
+ assert_eq!(sketch.rank(&req_f64(0.0), SearchCriteria::Inclusive), None);
+ assert!(matches!(
sketch.quantile(0.5, SearchCriteria::Inclusive),
- err(anything())
- );
- assert_that!(
+ Ok(None)
+ ));
+ assert!(matches!(
+ sketch.quantiles(&[0.25, 0.75], SearchCriteria::Inclusive),
+ Ok(None)
+ ));
+ assert!(matches!(
sketch.pmf(&[req_f64(0.0)], SearchCriteria::Inclusive),
- err(anything())
- );
- assert_that!(
+ Ok(None)
+ ));
+ assert!(matches!(
sketch.cdf(&[req_f64(0.0)], SearchCriteria::Inclusive),
- err(anything())
- );
+ Ok(None)
+ ));
}
#[test]
@@ -112,7 +113,8 @@ fn single_value_hra_answers_exactly() {
assert_eq!(
sketch
.quantile(rank, SearchCriteria::Exclusive)
- .expect("quantile should succeed"),
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty"),
req_f32(1.0)
);
}
@@ -221,14 +223,18 @@ fn small_edge_cases_answer_reasonably() -> Result<(),
Error> {
let mut single: ReqSketch<ReqF64> = ReqSketch::default();
single.update(req_f64(42.0));
assert_eq!(
- single.quantile(0.5, SearchCriteria::Inclusive)?,
+ single
+ .quantile(0.5, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty"),
req_f64(42.0)
);
let mut two_values = ReqSketch::default();
two_values.update(req_f64(1.0));
two_values.update(req_f64(100.0));
- let median = two_values.quantile(0.5, SearchCriteria::Inclusive)?;
+ let median = two_values
+ .quantile(0.5, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty");
assert_that!(*median, all!(ge(1.0), le(100.0)));
let mut duplicates = ReqSketch::default();
@@ -236,7 +242,9 @@ fn small_edge_cases_answer_reasonably() -> Result<(),
Error> {
duplicates.update(req_f64(42.0));
}
assert_eq!(
- duplicates.quantile(0.5, SearchCriteria::Inclusive)?,
+ duplicates
+ .quantile(0.5, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty"),
req_f64(42.0)
);
diff --git a/tests-integration/tests/req_test/generic.rs
b/tests-integration/tests/req_test/generic.rs
index a405e81..ab4335b 100644
--- a/tests-integration/tests/req_test/generic.rs
+++ b/tests-integration/tests/req_test/generic.rs
@@ -31,7 +31,10 @@ fn custom_items_do_not_need_serialization() {
assert_eq!(sketch.min_item(), Some(&Reading(10)));
assert_eq!(sketch.max_item(), Some(&Reading(30)));
assert_eq!(
- sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap(),
+ sketch
+ .quantile(0.5, SearchCriteria::Inclusive)
+ .unwrap()
+ .unwrap(),
Reading(20)
);
}
diff --git a/tests-integration/tests/req_test/merge.rs
b/tests-integration/tests/req_test/merge.rs
index d4c80e2..d24d6e7 100644
--- a/tests-integration/tests/req_test/merge.rs
+++ b/tests-integration/tests/req_test/merge.rs
@@ -43,13 +43,16 @@ fn merge_into_empty_preserves_source_distribution() {
let q25 = target
.quantile(0.25, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let q50 = target
.quantile(0.5, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let q75 = target
.quantile(0.75, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let r50 = target
.rank(&req_f64(500.0), SearchCriteria::Inclusive)
.expect("rank should succeed");
@@ -78,13 +81,16 @@ fn merge_two_ranges_preserves_distribution() {
let q25 = left
.quantile(0.25, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let q50 = left
.quantile(0.5, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let q75 = left
.quantile(0.75, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let r50 = left
.rank(&req_f64(1000.0), SearchCriteria::Inclusive)
.expect("rank should succeed");
@@ -122,6 +128,7 @@ fn many_small_merges_preserve_count_bounds_and_median() {
let median = sketch
.quantile(0.5, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
assert_that!(*median, near(4999.5, 500.0));
}
diff --git a/tests-integration/tests/req_test/property.rs
b/tests-integration/tests/req_test/property.rs
index cf7a8bd..24d91df 100644
--- a/tests-integration/tests/req_test/property.rs
+++ b/tests-integration/tests/req_test/property.rs
@@ -48,7 +48,8 @@ fn prop_quantile_rank_consistency() {
for rank in [0.1, 0.25, 0.5, 0.75, 0.9] {
let quantile = sketch
.quantile(rank, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
let recovered = sketch
.rank(&quantile, SearchCriteria::Inclusive)
.expect("rank should succeed");
@@ -96,7 +97,8 @@ fn prop_sketch_bounds() {
for rank in [0.0, 0.25, 0.5, 0.75, 1.0] {
let quantile = sketch
.quantile(rank, SearchCriteria::Inclusive)
- .expect("quantile should succeed");
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty");
assert!(
quantile >= true_min && quantile <= true_max,
"quantile {} out of bounds [{}, {}]",
diff --git a/tests-integration/tests/req_test/query.rs
b/tests-integration/tests/req_test/query.rs
index bc03743..20d2fda 100644
--- a/tests-integration/tests/req_test/query.rs
+++ b/tests-integration/tests/req_test/query.rs
@@ -64,7 +64,8 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
assert_eq!(
*sketch
.quantile(rank, SearchCriteria::Exclusive)
- .expect("quantile should succeed"),
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty"),
expected
);
}
@@ -73,7 +74,8 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
assert_eq!(
*sketch
.quantile(rank, SearchCriteria::Inclusive)
- .expect("quantile should succeed"),
+ .expect("quantile should succeed")
+ .expect("the sketch is non-empty"),
expected
);
}
@@ -81,7 +83,8 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
let splits = [2.0, 6.0, 9.0].map(req_f64);
let cdf = sketch
.cdf(&splits, SearchCriteria::Exclusive)
- .expect("cdf should succeed");
+ .expect("cdf should succeed")
+ .expect("the sketch is non-empty");
assert_that!(cdf[0], near(0.1, 1e-6));
assert_that!(cdf[1], near(0.5, 1e-6));
assert_that!(cdf[2], near(0.8, 1e-6));
@@ -89,7 +92,8 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
let pmf = sketch
.pmf(&splits, SearchCriteria::Exclusive)
- .expect("pmf should succeed");
+ .expect("pmf should succeed")
+ .expect("the sketch is non-empty");
assert_that!(pmf[0], near(0.1, 1e-6));
assert_that!(pmf[1], near(0.4, 1e-6));
assert_that!(pmf[2], near(0.3, 1e-6));
@@ -106,10 +110,12 @@ fn pmf_and_cdf_are_consistent() {
let split_points = [100.0, 300.0, 500.0, 700.0, 900.0].map(req_f64);
let pmf = sketch
.pmf(&split_points, SearchCriteria::Inclusive)
- .expect("pmf should succeed");
+ .expect("pmf should succeed")
+ .expect("the sketch is non-empty");
let cdf = sketch
.cdf(&split_points, SearchCriteria::Inclusive)
- .expect("cdf should succeed");
+ .expect("cdf should succeed")
+ .expect("the sketch is non-empty");
assert_that!(pmf.iter().sum::<f64>(), near(1.0, 1e-10));
@@ -139,9 +145,9 @@ fn cdf_preserves_ranks_and_exact_endpoint() {
.chain(std::iter::once(1.0))
.collect();
- assert_eq!(sketch.cdf(&splits, criteria).unwrap(), expected);
- assert_eq!(view.cdf(&splits, criteria).unwrap(), expected);
- assert_eq!(view.cdf(&[], criteria).unwrap(), [1.0]);
+ assert_eq!(sketch.cdf(&splits, criteria).unwrap().unwrap(), expected);
+ assert_eq!(view.cdf(&splits, criteria).unwrap().unwrap(), expected);
+ assert_eq!(view.cdf(&[], criteria).unwrap(), Some(vec![1.0]));
}
}
@@ -157,14 +163,14 @@ fn quantile_endpoints_preserve_extrema_after_compaction()
{
let view = sketch.sorted_view();
for criteria in [SearchCriteria::Inclusive,
SearchCriteria::Exclusive] {
- assert_eq!(sketch.quantile(0.0, criteria).unwrap(), -item);
- assert_eq!(sketch.quantile(1.0, criteria).unwrap(), item);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(),
Some(-item));
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(),
Some(item));
assert_eq!(
sketch.quantiles(&[0.0, 1.0], criteria).unwrap(),
- [-item, item]
+ Some(vec![-item, item])
);
- assert_eq!(view.quantile(0.0, criteria).unwrap(), -item);
- assert_eq!(view.quantile(1.0, criteria).unwrap(), item);
+ assert_eq!(view.quantile(0.0, criteria).unwrap(), Some(-item));
+ assert_eq!(view.quantile(1.0, criteria).unwrap(), Some(item));
}
}
assert!(sketch.is_estimation_mode());
@@ -172,10 +178,10 @@ fn quantile_endpoints_preserve_extrema_after_compaction()
{
let view = sketch.sorted_view();
sketch.reset();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive]
{
- assert!(sketch.quantile(0.0, criteria).is_err());
- assert!(sketch.quantile(1.0, criteria).is_err());
- assert_eq!(view.quantile(0.0, criteria).unwrap(), -64);
- assert_eq!(view.quantile(1.0, criteria).unwrap(), 64);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(), None);
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(), None);
+ assert_eq!(view.quantile(0.0, criteria).unwrap(), Some(-64));
+ assert_eq!(view.quantile(1.0, criteria).unwrap(), Some(64));
}
}
}
@@ -192,10 +198,10 @@ fn quantile_endpoints_with_large_stream_weight() {
let view = sketch.sorted_view();
for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
- assert_eq!(sketch.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(sketch.quantile(1.0, criteria).unwrap(), 1);
- assert_eq!(view.quantile(0.0, criteria).unwrap(), 0);
- assert_eq!(view.quantile(1.0, criteria).unwrap(), 1);
+ assert_eq!(sketch.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(sketch.quantile(1.0, criteria).unwrap(), Some(1));
+ assert_eq!(view.quantile(0.0, criteria).unwrap(), Some(0));
+ assert_eq!(view.quantile(1.0, criteria).unwrap(), Some(1));
}
}
@@ -230,7 +236,9 @@ fn quantiles_are_monotonic() -> Result<(), Error> {
let mut previous = 0.0;
for rank in ranks {
- let quantile = sketch.quantile(rank, SearchCriteria::Inclusive)?;
+ let quantile = sketch
+ .quantile(rank, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty");
assert_that!(*quantile, ge(previous));
previous = *quantile;
}
@@ -246,8 +254,12 @@ fn rank_quantile_round_trip_is_consistent() -> Result<(),
Error> {
}
for target_rank in [0.1, 0.25, 0.5, 0.75, 0.9] {
- let quantile = sketch.quantile(target_rank,
SearchCriteria::Inclusive)?;
- let recovered_rank = sketch.rank(&quantile,
SearchCriteria::Inclusive)?;
+ let quantile = sketch
+ .quantile(target_rank, SearchCriteria::Inclusive)?
+ .expect("the sketch is non-empty");
+ let recovered_rank = sketch
+ .rank(&quantile, SearchCriteria::Inclusive)
+ .expect("the sketch is non-empty");
let error = (recovered_rank - target_rank).abs() / target_rank;
assert_that!(error, lt(0.2));
}
@@ -263,8 +275,12 @@ fn search_criteria_rank_consistency() -> Result<(), Error>
{
}
for value in [100.0, 250.0, 500.0, 750.0].map(req_f64) {
- let inclusive_rank = sketch.rank(&value, SearchCriteria::Inclusive)?;
- let exclusive_rank = sketch.rank(&value, SearchCriteria::Exclusive)?;
+ let inclusive_rank = sketch
+ .rank(&value, SearchCriteria::Inclusive)
+ .expect("the sketch is non-empty");
+ let exclusive_rank = sketch
+ .rank(&value, SearchCriteria::Exclusive)
+ .expect("the sketch is non-empty");
assert_that!(exclusive_rank, le(inclusive_rank));
assert_that!(inclusive_rank, all!(ge(0.0), le(1.0)));
@@ -283,8 +299,18 @@ fn signed_zeros_share_rank_and_cannot_be_distinct_splits()
-> Result<(), Error>
sketch.update(positive_zero);
for value in [negative_zero, positive_zero] {
- assert_eq!(sketch.rank(&value, SearchCriteria::Exclusive)?, 0.0);
- assert_eq!(sketch.rank(&value, SearchCriteria::Inclusive)?, 1.0);
+ assert_eq!(
+ sketch
+ .rank(&value, SearchCriteria::Exclusive)
+ .expect("the sketch is non-empty"),
+ 0.0
+ );
+ assert_eq!(
+ sketch
+ .rank(&value, SearchCriteria::Inclusive)
+ .expect("the sketch is non-empty"),
+ 1.0
+ );
}
assert!(
diff --git a/tests-integration/tests/req_test/sorted_view_api.rs
b/tests-integration/tests/req_test/sorted_view_api.rs
index 59c4e6d..4c4d3d7 100644
--- a/tests-integration/tests/req_test/sorted_view_api.rs
+++ b/tests-integration/tests/req_test/sorted_view_api.rs
@@ -25,9 +25,6 @@ use datasketches::req::ReqSketch;
use datasketches::req::SortedView;
use googletest::assert_that;
use googletest::prelude::all;
-use googletest::prelude::anything;
-use googletest::prelude::contains_substring;
-use googletest::prelude::err;
use googletest::prelude::ge;
use googletest::prelude::lt;
use googletest::prelude::near;
@@ -47,19 +44,23 @@ fn populated_sketch(n: i64) -> ReqSketch<ReqF64> {
fn query_through_shared_ref(sketch: &ReqSketch<ReqF64>) {
sketch
.quantile(0.5, SearchCriteria::Inclusive)
- .expect("quantile");
+ .expect("quantile")
+ .expect("the sketch is non-empty");
sketch
.quantiles(&[0.25, 0.5, 0.75], SearchCriteria::Inclusive)
- .expect("quantiles");
+ .expect("quantiles")
+ .expect("the sketch is non-empty");
sketch
.rank(&req_f64(50.0), SearchCriteria::Inclusive)
.expect("rank");
sketch
.pmf(&[req_f64(10.0), req_f64(50.0)], SearchCriteria::Inclusive)
- .expect("pmf");
+ .expect("pmf")
+ .expect("the sketch is non-empty");
sketch
.cdf(&[req_f64(10.0), req_f64(50.0)], SearchCriteria::Inclusive)
- .expect("cdf");
+ .expect("cdf")
+ .expect("the sketch is non-empty");
assert!(!sketch.sorted_view().is_empty());
}
@@ -95,24 +96,36 @@ fn sorted_view_on_empty_sketch_is_an_empty_view() {
assert!(view.is_empty());
assert_eq!(view.len(), 0);
assert_eq!(view.total_weight(), 0);
- // Queries on the empty view still report an error.
- assert_that!(
- view.quantile(0.5, SearchCriteria::Inclusive),
- err(anything())
- );
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive] {
+ assert_eq!(sketch.rank(&req_f64(0.0), criteria), None);
+ assert_eq!(view.rank(&req_f64(0.0), criteria), None);
+ for rank in [0.0, 0.5, 1.0] {
+ assert_eq!(sketch.quantile(rank, criteria).unwrap(), None);
+ assert_eq!(view.quantile(rank, criteria).unwrap(), None);
+ }
+ assert_eq!(sketch.quantiles(&[0.0, 0.5, 1.0], criteria).unwrap(),
None);
+ assert_eq!(sketch.pmf(&[req_f64(0.0)], criteria).unwrap(), None);
+ assert_eq!(sketch.cdf(&[req_f64(0.0)], criteria).unwrap(), None);
+ assert_eq!(view.pmf(&[req_f64(0.0)], criteria).unwrap(), None);
+ assert_eq!(view.cdf(&[req_f64(0.0)], criteria).unwrap(), None);
+ }
}
#[test]
-fn empty_sketch_pmf_cdf_report_error() {
- let sketch: ReqSketch<ReqF64> = ReqSketch::default();
- assert_that!(
- sketch.pmf(&[req_f64(1.0)], SearchCriteria::Inclusive),
- err(anything())
- );
- assert_that!(
- sketch.cdf(&[req_f64(1.0)], SearchCriteria::Inclusive),
- err(anything())
- );
+fn empty_argument_slices_preserve_the_sketch_empty_state() {
+ for n in [0, 1] {
+ let sketch = populated_sketch(n);
+ let view = sketch.sorted_view();
+ let quantiles = (n != 0).then(Vec::new);
+ let distribution = (n != 0).then(|| vec![1.0]);
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive]
{
+ assert_eq!(sketch.quantiles(&[], criteria).unwrap(), quantiles);
+ assert_eq!(sketch.cdf(&[], criteria).unwrap(), distribution);
+ assert_eq!(sketch.pmf(&[], criteria).unwrap(), distribution);
+ assert_eq!(view.cdf(&[], criteria).unwrap(), distribution);
+ assert_eq!(view.pmf(&[], criteria).unwrap(), distribution);
+ }
+ }
}
#[test]
@@ -126,17 +139,33 @@ fn view_rank_is_primary_query_name() {
}
#[test]
-fn error_precedence_empty_before_invalid_rank() {
- // On an empty sketch the emptiness is reported before the out-of-range
rank.
- let empty: ReqSketch<ReqF64> = ReqSketch::default();
- let empty_err = empty.quantile(2.0,
SearchCriteria::Inclusive).unwrap_err();
- assert_that!(empty_err.message(), contains_substring("empty"));
-
- // On a populated sketch the out-of-range rank is reported.
- let sketch = populated_sketch(10);
- let range_err = sketch.quantile(2.0,
SearchCriteria::Inclusive).unwrap_err();
- assert_eq!(range_err.kind(), ErrorKind::InvalidArgument);
- assert_that!(range_err.message(), contains_substring("must be in"));
+fn invalid_arguments_are_rejected_regardless_of_sketch_state() {
+ for n in [0, 1, 1_000] {
+ let sketch = populated_sketch(n);
+ let view = sketch.sorted_view();
+ for criteria in [SearchCriteria::Inclusive, SearchCriteria::Exclusive]
{
+ for rank in [f64::NAN, f64::NEG_INFINITY, -0.1, 1.1,
f64::INFINITY] {
+ for error in [
+ sketch.quantile(rank, criteria).unwrap_err(),
+ view.quantile(rank, criteria).unwrap_err(),
+ sketch.quantiles(&[0.5, rank], criteria).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
+ }
+ for splits in [[1.0, 1.0], [2.0, 1.0]] {
+ let splits = splits.map(req_f64);
+ for error in [
+ sketch.cdf(&splits, criteria).unwrap_err(),
+ sketch.pmf(&splits, criteria).unwrap_err(),
+ view.cdf(&splits, criteria).unwrap_err(),
+ view.pmf(&splits, criteria).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
+ }
+ }
+ }
}
#[test]
@@ -157,6 +186,7 @@ fn concurrent_readers_share_the_sketch() {
sketch
.quantile(rank, SearchCriteria::Inclusive)
.expect("quantile from shared sketch")
+ .expect("the sketch is non-empty")
})
})
.collect();
diff --git a/tests-integration/tests/serde_tests/req.rs
b/tests-integration/tests/serde_tests/req.rs
index d0a03e1..a3a920f 100644
--- a/tests-integration/tests/serde_tests/req.rs
+++ b/tests-integration/tests/serde_tests/req.rs
@@ -510,7 +510,10 @@ fn validate_cross_language_fixture(path: PathBuf,
expected_n: u64) {
sketch.max_item().copied().map(ReqFloat::into_inner),
Some(expected_n as f32)
);
- let _ = sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap();
+ sketch
+ .quantile(0.5, SearchCriteria::Inclusive)
+ .unwrap()
+ .expect("non-empty snapshot should have a median");
}
let serialized = sketch.serialize();
diff --git a/tests-integration/tests/serde_tests/tdigest.rs
b/tests-integration/tests/serde_tests/tdigest.rs
index b90b1ca..8bcd989 100644
--- a/tests-integration/tests/serde_tests/tdigest.rs
+++ b/tests-integration/tests/serde_tests/tdigest.rs
@@ -64,13 +64,17 @@ fn test_sketch_file(path: PathBuf, n: u64, with_buffer:
bool, is_f32: bool) {
assert_eq!(td.total_weight(), n, "filepath: {path}");
assert_eq!(td.min_value(), Some(1.0), "filepath: {path}");
assert_eq!(td.max_value(), Some(n as f64), "filepath: {path}");
- assert_eq!(td.rank(0.0), Some(0.0), "filepath: {path}");
- assert_eq!(td.rank((n + 1) as f64), Some(1.0), "filepath: {path}");
+ assert_eq!(td.rank(0.0).unwrap(), Some(0.0), "filepath: {path}");
+ assert_eq!(
+ td.rank((n + 1) as f64).unwrap(),
+ Some(1.0),
+ "filepath: {path}"
+ );
if n == 1 {
- assert_eq!(td.rank(n as f64), Some(0.5), "filepath: {path}");
+ assert_eq!(td.rank(n as f64).unwrap(), Some(0.5), "filepath:
{path}");
} else {
assert_that!(
- td.rank(n as f64 / 2.).unwrap(),
+ td.rank(n as f64 / 2.).unwrap().unwrap(),
near(0.5, 0.05),
"filepath: {path}",
);
@@ -126,23 +130,31 @@ fn test_deserialize_from_reference_implementation() {
assert_eq!(td.total_weight(), n, "filepath: {path}");
assert_eq!(td.min_value(), Some(0.0), "filepath: {path}");
assert_eq!(td.max_value(), Some((n - 1) as f64), "filepath: {path}");
- assert_that!(td.rank(0.0).unwrap(), near(0.0, 0.0001), "filepath:
{path}");
assert_that!(
- td.rank(n as f64 / 4.).unwrap(),
+ td.rank(0.0).unwrap().unwrap(),
+ near(0.0, 0.0001),
+ "filepath: {path}"
+ );
+ assert_that!(
+ td.rank(n as f64 / 4.).unwrap().unwrap(),
near(0.25, 0.0001),
"filepath: {path}"
);
assert_that!(
- td.rank(n as f64 / 2.).unwrap(),
+ td.rank(n as f64 / 2.).unwrap().unwrap(),
near(0.5, 0.0001),
"filepath: {path}"
);
assert_that!(
- td.rank((n * 3) as f64 / 4.).unwrap(),
+ td.rank((n * 3) as f64 / 4.).unwrap().unwrap(),
near(0.75, 0.0001),
"filepath: {path}"
);
- assert_that!(td.rank(n as f64).unwrap(), eq(1.0), "filepath: {path}");
+ assert_that!(
+ td.rank(n as f64).unwrap().unwrap(),
+ eq(1.0),
+ "filepath: {path}"
+ );
}
}
@@ -223,8 +235,14 @@ fn test_many_values() {
assert_eq!(td.is_empty(), deserialized_td.is_empty());
assert_eq!(td.min_value(), deserialized_td.min_value());
assert_eq!(td.max_value(), deserialized_td.max_value());
- assert_eq!(td.rank(500.0), deserialized_td.rank(500.0));
- assert_eq!(td.quantile(0.5), deserialized_td.quantile(0.5));
+ assert_eq!(
+ td.rank(500.0).unwrap(),
+ deserialized_td.rank(500.0).unwrap()
+ );
+ assert_eq!(
+ td.quantile(0.5).unwrap(),
+ deserialized_td.quantile(0.5).unwrap()
+ );
}
#[test]
@@ -239,7 +257,10 @@ fn test_frozen_roundtrip() {
assert_eq!(actual.total_weight(), expected.total_weight());
assert_eq!(actual.min_value(), expected.min_value());
assert_eq!(actual.max_value(), expected.max_value());
- assert_eq!(actual.quantile(0.5), expected.quantile(0.5));
+ assert_eq!(
+ actual.quantile(0.5).unwrap(),
+ expected.quantile(0.5).unwrap()
+ );
}
#[test]
@@ -251,7 +272,7 @@ fn
test_serialized_bytes_stable_for_full_and_merged_digests() {
let mut left = patterned_digest(10, 201, 2);
let mut right = patterned_digest(10, 199, 3);
- right.rank(0.0);
+ right.rank(0.0).unwrap();
left.merge(&right);
let bytes = left.serialize();
assert_eq!(bytes.len(), 272);
@@ -394,7 +415,10 @@ fn test_deserialize_single_sample_requires_equal_extrema()
{
if min == max {
let mut digest = result.unwrap();
assert_eq!(digest.total_weight(), 1);
- assert_eq!(digest.quantiles(&[0., 0.5, 1.]),
Some(vec![50.; 3]));
+ assert_eq!(
+ digest.quantiles(&[0., 0.5, 1.]).unwrap(),
+ Some(vec![50.; 3])
+ );
} else {
assert_eq!(
result.unwrap_err().kind(),
@@ -428,7 +452,10 @@ fn
test_deserialize_compat_single_sample_requires_equal_extrema() {
if min == max {
let mut digest = TDigestMut::deserialize(&bytes).unwrap();
assert_eq!(digest.total_weight(), 1);
- assert_eq!(digest.quantiles(&[0., 0.5, 1.]), Some(vec![50.;
3]));
+ assert_eq!(
+ digest.quantiles(&[0., 0.5, 1.]).unwrap(),
+ Some(vec![50.; 3])
+ );
} else {
assert_invalid_tdigest(&bytes);
}
@@ -504,6 +531,6 @@ fn test_large_weights_produce_finite_extreme_quantile() {
bytes[56..64].copy_from_slice(&(1_u64 << 52).to_le_bytes());
let mut tdigest = TDigestMut::deserialize(&bytes).unwrap();
- let quantile = tdigest.quantile(0.25).unwrap();
+ let quantile = tdigest.quantile(0.25).unwrap().unwrap();
assert_that!(quantile, all!(is_finite(), ge(lower), le(f64::MAX)));
}
diff --git a/tests-integration/tests/tdigest_test/property.rs
b/tests-integration/tests/tdigest_test/property.rs
index 61577ef..431da0e 100644
--- a/tests-integration/tests/tdigest_test/property.rs
+++ b/tests-integration/tests/tdigest_test/property.rs
@@ -39,10 +39,10 @@ fn assert_quantiles_are_monotonic(tdigest: &mut TDigestMut)
{
let ranks = (0..=RANK_STEPS)
.map(|step| step as f64 / RANK_STEPS as f64)
.collect::<Vec<_>>();
- let quantiles = tdigest.quantiles(&ranks).unwrap();
+ let quantiles = tdigest.quantiles(&ranks).unwrap().unwrap();
for (&rank, quantile) in ranks.iter().zip(quantiles) {
- assert_eq!(tdigest.quantile(rank), Some(quantile));
+ assert_eq!(tdigest.quantile(rank).unwrap(), Some(quantile));
assert!(
(previous..=max).contains(&quantile),
"quantile {quantile} at rank {rank} is outside [{previous}, {max}]"
@@ -77,12 +77,12 @@ fn prop_finite_values_survive_partial_aggregation() {
// Use the input samples as split points, independently of the
computed quantiles.
values.sort_unstable_by(f64::total_cmp);
values.dedup();
- assert_eq!(digest.quantile(0.), values.first().copied());
- assert_eq!(digest.quantile(1.), values.last().copied());
- let cdf = digest.cdf(&values).unwrap();
+ assert_eq!(digest.quantile(0.).unwrap(), values.first().copied());
+ assert_eq!(digest.quantile(1.).unwrap(), values.last().copied());
+ let cdf = digest.cdf(&values).unwrap().unwrap();
assert!(cdf.is_sorted());
assert!(cdf.iter().all(|rank| (0.0..=1.0).contains(rank)));
- let pmf = digest.pmf(&values).unwrap();
+ let pmf = digest.pmf(&values).unwrap().unwrap();
assert!(pmf.iter().all(|p| (0.0..=1.0).contains(p)));
assert!((pmf.iter().sum::<f64>() - 1.).abs() < 1e-12);
TestResult::passed()
@@ -159,12 +159,12 @@ fn
prop_batch_quantiles_preserve_input_order_and_duplicates() {
Some(
ranks
.iter()
- .map(|&rank| frozen.quantile(rank).unwrap())
+ .map(|&rank| frozen.quantile(rank).unwrap().unwrap())
.collect::<Vec<_>>(),
)
};
- assert_eq!(tdigest.clone().quantiles(&ranks), expected);
- assert_eq!(frozen.quantiles(&ranks), expected);
+ assert_eq!(tdigest.clone().quantiles(&ranks).unwrap(), expected);
+ assert_eq!(frozen.quantiles(&ranks).unwrap(), expected);
}
}
diff --git a/tests-integration/tests/tdigest_test/sketch.rs
b/tests-integration/tests/tdigest_test/sketch.rs
index b5bb67e..86bc45f 100644
--- a/tests-integration/tests/tdigest_test/sketch.rs
+++ b/tests-integration/tests/tdigest_test/sketch.rs
@@ -19,6 +19,7 @@ use std::mem::size_of;
use std::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
+use datasketches::error::ErrorKind;
use datasketches::tdigest::TDigestMut;
use googletest::assert_that;
use googletest::prelude::eq;
@@ -33,14 +34,16 @@ fn test_empty() {
assert_eq!(tdigest.total_weight(), 0);
assert_eq!(tdigest.min_value(), None);
assert_eq!(tdigest.max_value(), None);
- assert_eq!(tdigest.rank(0.0), None);
- assert_eq!(tdigest.quantile(0.5), None);
- assert_eq!(tdigest.quantiles(&[0.0, 0.5, 1.0]), None);
- assert_eq!(tdigest.quantiles(&[]), None);
+ assert_eq!(tdigest.rank(0.0).unwrap(), None);
+ assert_eq!(tdigest.quantile(0.5).unwrap(), None);
+ assert_eq!(tdigest.quantiles(&[0.0, 0.5, 1.0]).unwrap(), None);
+ assert_eq!(tdigest.quantiles(&[]).unwrap(), None);
let split_points = [0.0];
- assert_eq!(tdigest.pmf(&split_points), None);
- assert_eq!(tdigest.cdf(&split_points), None);
+ assert_eq!(tdigest.pmf(&split_points).unwrap(), None);
+ assert_eq!(tdigest.cdf(&split_points).unwrap(), None);
+ assert_eq!(tdigest.pmf(&[]).unwrap(), None);
+ assert_eq!(tdigest.cdf(&[]).unwrap(), None);
let tdigest = TDigestMut::new(10).unwrap().freeze();
assert!(tdigest.is_empty());
@@ -48,14 +51,16 @@ fn test_empty() {
assert_eq!(tdigest.total_weight(), 0);
assert_eq!(tdigest.min_value(), None);
assert_eq!(tdigest.max_value(), None);
- assert_eq!(tdigest.rank(0.0), None);
- assert_eq!(tdigest.quantile(0.5), None);
- assert_eq!(tdigest.quantiles(&[0.0, 0.5, 1.0]), None);
- assert_eq!(tdigest.quantiles(&[]), None);
+ assert_eq!(tdigest.rank(0.0).unwrap(), None);
+ assert_eq!(tdigest.quantile(0.5).unwrap(), None);
+ assert_eq!(tdigest.quantiles(&[0.0, 0.5, 1.0]).unwrap(), None);
+ assert_eq!(tdigest.quantiles(&[]).unwrap(), None);
let split_points = [0.0];
- assert_eq!(tdigest.pmf(&split_points), None);
- assert_eq!(tdigest.cdf(&split_points), None);
+ assert_eq!(tdigest.pmf(&split_points).unwrap(), None);
+ assert_eq!(tdigest.cdf(&split_points).unwrap(), None);
+ assert_eq!(tdigest.pmf(&[]).unwrap(), None);
+ assert_eq!(tdigest.cdf(&[]).unwrap(), None);
}
#[test]
@@ -66,31 +71,36 @@ fn test_one_value() {
assert_eq!(tdigest.total_weight(), 1);
assert_eq!(tdigest.min_value(), Some(1.0));
assert_eq!(tdigest.max_value(), Some(1.0));
- assert_eq!(tdigest.rank(0.99), Some(0.0));
- assert_eq!(tdigest.rank(1.0), Some(0.5));
- assert_eq!(tdigest.rank(1.01), Some(1.0));
- assert_eq!(tdigest.quantile(0.0), Some(1.0));
- assert_eq!(tdigest.quantile(0.5), Some(1.0));
- assert_eq!(tdigest.quantile(1.0), Some(1.0));
+ assert_eq!(tdigest.rank(0.99).unwrap(), Some(0.0));
+ assert_eq!(tdigest.rank(1.0).unwrap(), Some(0.5));
+ assert_eq!(tdigest.rank(1.01).unwrap(), Some(1.0));
+ assert_eq!(tdigest.quantile(0.0).unwrap(), Some(1.0));
+ assert_eq!(tdigest.quantile(0.5).unwrap(), Some(1.0));
+ assert_eq!(tdigest.quantile(1.0).unwrap(), Some(1.0));
let ranks = [1.0, 0.5, 0.0, 0.5];
- assert_eq!(tdigest.quantiles(&ranks), Some(vec![1.0; ranks.len()]));
assert_eq!(
- tdigest.freeze().quantiles(&ranks),
+ tdigest.quantiles(&ranks).unwrap(),
+ Some(vec![1.0; ranks.len()])
+ );
+ assert_eq!(
+ tdigest.freeze().quantiles(&ranks).unwrap(),
Some(vec![1.0; ranks.len()])
);
}
#[test]
-fn test_empty_split_points_define_one_bin() {
+fn test_empty_query_slices_on_nonempty_digest() {
let mut tdigest = TDigestMut::new(100).unwrap();
tdigest.update(1.0);
- assert_eq!(tdigest.cdf(&[]), Some(vec![1.0]));
- assert_eq!(tdigest.pmf(&[]), Some(vec![1.0]));
+ assert_eq!(tdigest.cdf(&[]).unwrap(), Some(vec![1.0]));
+ assert_eq!(tdigest.pmf(&[]).unwrap(), Some(vec![1.0]));
+ assert_eq!(tdigest.quantiles(&[]).unwrap(), Some(vec![]));
let tdigest = tdigest.freeze();
- assert_eq!(tdigest.cdf(&[]), Some(vec![1.0]));
- assert_eq!(tdigest.pmf(&[]), Some(vec![1.0]));
+ assert_eq!(tdigest.cdf(&[]).unwrap(), Some(vec![1.0]));
+ assert_eq!(tdigest.pmf(&[]).unwrap(), Some(vec![1.0]));
+ assert_eq!(tdigest.quantiles(&[]).unwrap(), Some(vec![]));
}
#[test]
@@ -100,7 +110,7 @@ fn test_maximum_k() {
let tdigest = tdigest.freeze();
assert_eq!(tdigest.k(), u16::MAX);
- assert_eq!(tdigest.quantile(0.5), Some(1.0));
+ assert_eq!(tdigest.quantile(0.5).unwrap(), Some(1.0));
}
#[test]
@@ -118,14 +128,14 @@ fn test_estimated_size_reuses_buffer_after_compression() {
}
let size_before_compression = tdigest.estimated_size();
assert!(size_before_compression > inline_size);
- tdigest.rank(0.5);
+ tdigest.rank(0.5).unwrap();
assert!(tdigest.estimated_size() <= size_before_compression);
for value in MAX_UNMERGED..10_000 {
tdigest.update(value as f64);
}
let size_before_compression = tdigest.estimated_size();
- tdigest.rank(0.5);
+ tdigest.rank(0.5).unwrap();
assert!(tdigest.estimated_size() <= size_before_compression);
let mut left = TDigestMut::new(K).unwrap();
@@ -170,35 +180,41 @@ fn test_many_values() {
assert_eq!(tdigest.min_value(), Some(0.0));
assert_eq!(tdigest.max_value(), Some((n - 1) as f64));
- assert_that!(tdigest.rank(0.0).unwrap(), near(0.0, 0.0001));
- assert_that!(tdigest.rank((n / 4) as f64).unwrap(), near(0.25, 0.0001));
- assert_that!(tdigest.rank((n / 2) as f64).unwrap(), near(0.5, 0.0001));
+ assert_that!(tdigest.rank(0.0).unwrap().unwrap(), near(0.0, 0.0001));
+ assert_that!(
+ tdigest.rank((n / 4) as f64).unwrap().unwrap(),
+ near(0.25, 0.0001)
+ );
+ assert_that!(
+ tdigest.rank((n / 2) as f64).unwrap().unwrap(),
+ near(0.5, 0.0001)
+ );
assert_that!(
- tdigest.rank((n * 3 / 4) as f64).unwrap(),
+ tdigest.rank((n * 3 / 4) as f64).unwrap().unwrap(),
near(0.75, 0.0001)
);
- assert_that!(tdigest.rank(n as f64).unwrap(), eq(1.0));
- assert_that!(tdigest.quantile(0.0).unwrap(), eq(0.0));
+ assert_that!(tdigest.rank(n as f64).unwrap().unwrap(), eq(1.0));
+ assert_that!(tdigest.quantile(0.0).unwrap().unwrap(), eq(0.0));
assert_that!(
- tdigest.quantile(0.5).unwrap(),
+ tdigest.quantile(0.5).unwrap().unwrap(),
near((n / 2) as f64, 0.03 * (n / 2) as f64)
);
assert_that!(
- tdigest.quantile(0.9).unwrap(),
+ tdigest.quantile(0.9).unwrap().unwrap(),
near((n as f64) * 0.9, 0.01 * (n as f64) * 0.9)
);
assert_that!(
- tdigest.quantile(0.95).unwrap(),
+ tdigest.quantile(0.95).unwrap().unwrap(),
near((n as f64) * 0.95, 0.01 * (n as f64) * 0.95)
);
- assert_that!(tdigest.quantile(1.0).unwrap(), eq((n - 1) as f64));
+ assert_that!(tdigest.quantile(1.0).unwrap().unwrap(), eq((n - 1) as f64));
let split_points = [n as f64 / 2.0];
- let pmf = tdigest.pmf(&split_points).unwrap();
+ let pmf = tdigest.pmf(&split_points).unwrap().unwrap();
assert_eq!(pmf.len(), 2);
assert_that!(pmf[0], near(0.5, 0.0001));
assert_that!(pmf[1], near(0.5, 0.0001));
- let cdf = tdigest.cdf(&split_points).unwrap();
+ let cdf = tdigest.cdf(&split_points).unwrap().unwrap();
assert_eq!(cdf.len(), 2);
assert_that!(cdf[0], near(0.5, 0.0001));
assert_that!(cdf[1], eq(1.0));
@@ -209,13 +225,13 @@ fn test_rank_two_values() {
let mut tdigest = TDigestMut::new(100).unwrap();
tdigest.update(1.0);
tdigest.update(2.0);
- assert_eq!(tdigest.rank(0.99), Some(0.0));
- assert_eq!(tdigest.rank(1.0), Some(0.25));
- assert_eq!(tdigest.rank(1.25), Some(0.375));
- assert_eq!(tdigest.rank(1.5), Some(0.5));
- assert_eq!(tdigest.rank(1.75), Some(0.625));
- assert_eq!(tdigest.rank(2.0), Some(0.75));
- assert_eq!(tdigest.rank(2.01), Some(1.0));
+ assert_eq!(tdigest.rank(0.99).unwrap(), Some(0.0));
+ assert_eq!(tdigest.rank(1.0).unwrap(), Some(0.25));
+ assert_eq!(tdigest.rank(1.25).unwrap(), Some(0.375));
+ assert_eq!(tdigest.rank(1.5).unwrap(), Some(0.5));
+ assert_eq!(tdigest.rank(1.75).unwrap(), Some(0.625));
+ assert_eq!(tdigest.rank(2.0).unwrap(), Some(0.75));
+ assert_eq!(tdigest.rank(2.01).unwrap(), Some(1.0));
}
#[test]
@@ -225,9 +241,9 @@ fn test_rank_repeated_values() {
tdigest.update(1.0);
tdigest.update(1.0);
tdigest.update(1.0);
- assert_eq!(tdigest.rank(0.99), Some(0.0));
- assert_eq!(tdigest.rank(1.0), Some(0.5));
- assert_eq!(tdigest.rank(1.01), Some(1.0));
+ assert_eq!(tdigest.rank(0.99).unwrap(), Some(0.0));
+ assert_eq!(tdigest.rank(1.0).unwrap(), Some(0.5));
+ assert_eq!(tdigest.rank(1.01).unwrap(), Some(1.0));
}
#[test]
@@ -237,11 +253,11 @@ fn test_repeated_blocks() {
tdigest.update(2.0);
tdigest.update(2.0);
tdigest.update(3.0);
- assert_eq!(tdigest.rank(0.99), Some(0.0));
- assert_eq!(tdigest.rank(1.0), Some(0.125));
- assert_eq!(tdigest.rank(2.0), Some(0.5));
- assert_eq!(tdigest.rank(3.0), Some(0.875));
- assert_eq!(tdigest.rank(3.01), Some(1.0));
+ assert_eq!(tdigest.rank(0.99).unwrap(), Some(0.0));
+ assert_eq!(tdigest.rank(1.0).unwrap(), Some(0.125));
+ assert_eq!(tdigest.rank(2.0).unwrap(), Some(0.5));
+ assert_eq!(tdigest.rank(3.0).unwrap(), Some(0.875));
+ assert_eq!(tdigest.rank(3.01).unwrap(), Some(1.0));
}
#[test]
@@ -266,13 +282,13 @@ fn test_rank_brackets_weighted_equal_mean_runs() {
let expected = [0., 3., 7.5, 18., 32.5, 47., 64.].map(|mass| mass / 64.);
let frozen = digest.clone().freeze();
for (&point, &rank) in points.iter().zip(&expected) {
- assert_eq!(digest.rank(point), Some(rank));
- assert_eq!(frozen.rank(point), Some(rank));
+ assert_eq!(digest.rank(point).unwrap(), Some(rank));
+ assert_eq!(frozen.rank(point).unwrap(), Some(rank));
}
let mut cdf = expected.to_vec();
cdf.push(1.);
- assert_eq!(digest.cdf(&points), Some(cdf.clone()));
- assert_eq!(frozen.cdf(&points), Some(cdf));
+ assert_eq!(digest.cdf(&points).unwrap(), Some(cdf.clone()));
+ assert_eq!(frozen.cdf(&points).unwrap(), Some(cdf));
let pmf = vec![
0.,
3. / 64.,
@@ -283,8 +299,8 @@ fn test_rank_brackets_weighted_equal_mean_runs() {
17. / 64.,
0.,
];
- assert_eq!(digest.pmf(&points), Some(pmf.clone()));
- assert_eq!(frozen.pmf(&points), Some(pmf));
+ assert_eq!(digest.pmf(&points).unwrap(), Some(pmf.clone()));
+ assert_eq!(frozen.pmf(&points).unwrap(), Some(pmf));
}
#[test]
@@ -299,11 +315,11 @@ fn test_merge_small() {
assert_eq!(td1.min_value(), Some(1.0));
assert_eq!(td1.max_value(), Some(3.0));
assert_eq!(td1.total_weight(), 4);
- assert_eq!(td1.rank(0.99), Some(0.0));
- assert_eq!(td1.rank(1.0), Some(0.125));
- assert_eq!(td1.rank(2.0), Some(0.5));
- assert_eq!(td1.rank(3.0), Some(0.875));
- assert_eq!(td1.rank(3.01), Some(1.0));
+ assert_eq!(td1.rank(0.99).unwrap(), Some(0.0));
+ assert_eq!(td1.rank(1.0).unwrap(), Some(0.125));
+ assert_eq!(td1.rank(2.0).unwrap(), Some(0.5));
+ assert_eq!(td1.rank(3.0).unwrap(), Some(0.875));
+ assert_eq!(td1.rank(3.01).unwrap(), Some(1.0));
}
#[test]
@@ -323,11 +339,20 @@ fn test_merge_large() {
assert_eq!(td1.min_value(), Some(0.0));
assert_eq!(td1.max_value(), Some((n - 1) as f64));
- assert_that!(td1.rank(0.0).unwrap(), near(0.0, 0.0001));
- assert_that!(td1.rank((n / 4) as f64).unwrap(), near(0.25, 0.0001));
- assert_that!(td1.rank((n / 2) as f64).unwrap(), near(0.5, 0.0001));
- assert_that!(td1.rank((n * 3 / 4) as f64).unwrap(), near(0.75, 0.0001));
- assert_that!(td1.rank(n as f64).unwrap(), eq(1.0));
+ assert_that!(td1.rank(0.0).unwrap().unwrap(), near(0.0, 0.0001));
+ assert_that!(
+ td1.rank((n / 4) as f64).unwrap().unwrap(),
+ near(0.25, 0.0001)
+ );
+ assert_that!(
+ td1.rank((n / 2) as f64).unwrap().unwrap(),
+ near(0.5, 0.0001)
+ );
+ assert_that!(
+ td1.rank((n * 3 / 4) as f64).unwrap().unwrap(),
+ near(0.75, 0.0001)
+ );
+ assert_that!(td1.rank(n as f64).unwrap().unwrap(), eq(1.0));
}
#[test]
@@ -346,8 +371,11 @@ fn test_mixed_k_merge_retains_receiver_k() {
assert_eq!(left.k(), left_k);
assert_eq!(left.total_weight(), 10_000 + right_count);
- assert_eq!(left.quantile(0.0), Some(0.0));
- assert_eq!(left.quantile(1.0), Some((9_999 + right_count) as f64));
+ assert_eq!(left.quantile(0.0).unwrap(), Some(0.0));
+ assert_eq!(
+ left.quantile(1.0).unwrap(),
+ Some((9_999 + right_count) as f64)
+ );
}
}
@@ -360,8 +388,8 @@ fn
test_from_iter_uses_one_result_with_the_smallest_nonempty_k() {
first.update(value as f64);
second.update((value + 1_000) as f64);
}
- let _ = first.quantile(0.5);
- let _ = second.quantile(0.5);
+ let _ = first.quantile(0.5).unwrap();
+ let _ = second.quantile(0.5).unwrap();
let mut merged = [first, empty,
second].into_iter().collect::<TDigestMut>();
@@ -370,7 +398,7 @@ fn
test_from_iter_uses_one_result_with_the_smallest_nonempty_k() {
assert_eq!(merged.min_value(), Some(0.0));
assert_eq!(merged.max_value(), Some(1_999.0));
let quantiles = (0..=100)
- .map(|rank| merged.quantile(rank as f64 / 100.).unwrap())
+ .map(|rank| merged.quantile(rank as f64 / 100.).unwrap().unwrap())
.collect::<Vec<_>>();
assert!(quantiles.windows(2).all(|pair| pair[0] <= pair[1]));
}
@@ -431,8 +459,8 @@ fn
test_from_iter_matches_single_compression_for_interleaved_runs() {
let mut merged =
runs.into_iter().map(make_digest).collect::<TDigestMut>();
assert_eq!(merged.serialize(), expected.serialize());
- assert_eq!(merged.quantile(0.0), Some(-10.0));
- assert_eq!(merged.quantile(1.0), Some(20.0));
+ assert_eq!(merged.quantile(0.0).unwrap(), Some(-10.0));
+ assert_eq!(merged.quantile(1.0).unwrap(), Some(20.0));
// A collected digest must still support enough updates to grow
and compress again.
for value in 0..1_000 {
@@ -455,11 +483,11 @@ fn
test_from_iter_handles_mixed_compressed_and_buffered_inputs() {
digest.update(value);
expected.update(value);
if run == 2 && row == 29 {
- let _ = digest.quantile(0.5);
+ let _ = digest.quantile(0.5).unwrap();
}
}
if run == 1 {
- let _ = digest.quantile(0.5);
+ let _ = digest.quantile(0.5).unwrap();
}
digest
})
@@ -506,12 +534,12 @@ fn
test_serialized_batch_merge_tree_preserves_weight_and_quantiles() {
let mut merged = TDigestMut::deserialize(&states[0]).unwrap();
assert_eq!(merged.total_weight(), values.len() as u64);
- assert_eq!(merged.quantile(0.0), values.first().copied());
- assert_eq!(merged.quantile(1.0), values.last().copied());
+ assert_eq!(merged.quantile(0.0).unwrap(), values.first().copied());
+ assert_eq!(merged.quantile(1.0).unwrap(), values.last().copied());
let mut previous = values[0];
for rank in [0.01, 0.5, 0.9, 0.99] {
- let estimate = merged.quantile(rank).unwrap();
+ let estimate = merged.quantile(rank).unwrap().unwrap();
assert!((previous..=values[values.len() - 1]).contains(&estimate));
// Check this fixture's empirical rank, allowing equal values to
span a rank interval.
let lower =
@@ -535,7 +563,7 @@ fn test_from_iter_checks_total_weight_before_compression() {
let mut digest = TDigestMut::new(100).unwrap();
digest.update(value);
if compressed {
- let _ = digest.quantile(0.5);
+ let _ = digest.quantile(0.5).unwrap();
}
digest
});
@@ -543,8 +571,8 @@ fn test_from_iter_checks_total_weight_before_compression() {
.into_iter()
.collect::<TDigestMut>();
assert_eq!(at_limit.total_weight(), u64::MAX);
- assert_eq!(at_limit.quantile(0.0), Some(0.0));
- assert_eq!(at_limit.quantile(1.0), Some(2.0));
+ assert_eq!(at_limit.quantile(0.0).unwrap(), Some(0.0));
+ assert_eq!(at_limit.quantile(1.0).unwrap(), Some(2.0));
assert!(
catch_unwind(|| [heavy, one, two,
extra].into_iter().collect::<TDigestMut>()).is_err()
@@ -614,7 +642,7 @@ fn test_extreme_values_produce_finite_quantiles() {
assert_eq!(tdigest.min_value(), Some(-f64::MAX));
assert_eq!(tdigest.max_value(), Some(f64::MAX));
for rank in [0.25, 0.5, 0.75] {
- let quantile = tdigest.quantile(rank).unwrap();
+ let quantile = tdigest.quantile(rank).unwrap().unwrap();
assert_that!(quantile, is_finite(), "quantile at rank {rank}");
}
}
@@ -630,7 +658,7 @@ fn test_rank_interpolation_across_extreme_finite_values() {
let points = [-scale, -scale / 2., 0., scale / 2., scale];
for (point, expected) in points.into_iter().zip([0.25, 0.375, 0.5,
0.625, 0.75]) {
assert_that!(
- digest.rank(point).unwrap(),
+ digest.rank(point).unwrap().unwrap(),
near(expected, 4. * f64::EPSILON)
);
}
@@ -647,11 +675,11 @@ fn test_tail_interpolation_across_extreme_finite_values()
{
for (digest, rank, expected) in [(left, 3. / 11., -0.25), (right, 8. /
11., 0.25)] {
let digest = digest.freeze();
assert_that!(
- digest.quantile(rank).unwrap() / max,
+ digest.quantile(rank).unwrap().unwrap() / max,
near(expected, 4. * f64::EPSILON)
);
assert_that!(
- digest.rank(expected * max).unwrap(),
+ digest.rank(expected * max).unwrap().unwrap(),
near(rank, 4. * f64::EPSILON)
);
}
@@ -663,7 +691,7 @@ fn test_estimate_repeat_values() {
for _ in 0..20 {
tdigest.update(1.0);
}
- assert_eq!(tdigest.quantile(0.9), Some(1.0));
+ assert_eq!(tdigest.quantile(0.9).unwrap(), Some(1.0));
}
/// Builds a weighted image, including tail centroids unavailable through raw
updates.
@@ -690,8 +718,8 @@ fn deserialize_with_centroids(k: u16, min: f64, max: f64,
centroids: &[(f64, u64
fn assert_quantile_queries(tdigest: &TDigestMut, ranks: &[f64], expected:
&[f64]) {
let frozen = tdigest.clone().freeze();
for actual in [
- tdigest.clone().quantiles(ranks).unwrap(),
- frozen.quantiles(ranks).unwrap(),
+ tdigest.clone().quantiles(ranks).unwrap().unwrap(),
+ frozen.quantiles(ranks).unwrap().unwrap(),
] {
assert_eq!(actual.len(), expected.len());
for (actual, &expected) in actual.into_iter().zip(expected) {
@@ -700,8 +728,14 @@ fn assert_quantile_queries(tdigest: &TDigestMut, ranks:
&[f64], expected: &[f64]
}
let mut mutable = tdigest.clone();
for (&rank, &expected) in ranks.iter().zip(expected) {
- assert_that!(mutable.quantile(rank).unwrap(), near(expected, 1e-12));
- assert_that!(frozen.quantile(rank).unwrap(), near(expected, 1e-12));
+ assert_that!(
+ mutable.quantile(rank).unwrap().unwrap(),
+ near(expected, 1e-12)
+ );
+ assert_that!(
+ frozen.quantile(rank).unwrap().unwrap(),
+ near(expected, 1e-12)
+ );
}
}
@@ -712,11 +746,26 @@ fn
test_quantile_moves_toward_the_nearer_bracketing_centroid() {
assert_eq!(tdigest.total_weight(), 12);
// Ranks 2/12 and 6/12 sit exactly on the two centroids bracketing the
first interval.
- assert_that!(tdigest.quantile(2.0 / 12.0).unwrap(), near(0.0, 1e-12));
- assert_that!(tdigest.quantile(3.0 / 12.0).unwrap(), near(2.5, 1e-12));
- assert_that!(tdigest.quantile(4.0 / 12.0).unwrap(), near(5.0, 1e-12));
- assert_that!(tdigest.quantile(5.0 / 12.0).unwrap(), near(7.5, 1e-12));
- assert_that!(tdigest.quantile(6.0 / 12.0).unwrap(), near(10.0, 1e-12));
+ assert_that!(
+ tdigest.quantile(2.0 / 12.0).unwrap().unwrap(),
+ near(0.0, 1e-12)
+ );
+ assert_that!(
+ tdigest.quantile(3.0 / 12.0).unwrap().unwrap(),
+ near(2.5, 1e-12)
+ );
+ assert_that!(
+ tdigest.quantile(4.0 / 12.0).unwrap().unwrap(),
+ near(5.0, 1e-12)
+ );
+ assert_that!(
+ tdigest.quantile(5.0 / 12.0).unwrap().unwrap(),
+ near(7.5, 1e-12)
+ );
+ assert_that!(
+ tdigest.quantile(6.0 / 12.0).unwrap().unwrap(),
+ near(10.0, 1e-12)
+ );
}
#[test]
@@ -725,11 +774,20 @@ fn test_quantile_right_tail_stays_within_max() {
deserialize_with_centroids(100, 0.0, 100.0, &[(10.0, 10), (50.0, 10),
(90.0, 10)]);
assert_eq!(tdigest.max_value(), Some(100.0));
- assert_that!(tdigest.quantile(0.9).unwrap(), near(95.0, 1e-12));
- assert_that!(tdigest.quantile(29.0 / 30.0).unwrap(), near(100.0, 1e-12));
+ assert_that!(tdigest.quantile(0.9).unwrap().unwrap(), near(95.0, 1e-12));
+ assert_that!(
+ tdigest.quantile(29.0 / 30.0).unwrap().unwrap(),
+ near(100.0, 1e-12)
+ );
// Mirrors the left tail, which interpolates from min up to the first
centroid mean.
- assert_that!(tdigest.quantile(1.0 / 30.0).unwrap(), near(0.0, 1e-12));
- assert_that!(tdigest.quantile(5.0 / 30.0).unwrap(), near(10.0, 1e-12));
+ assert_that!(
+ tdigest.quantile(1.0 / 30.0).unwrap().unwrap(),
+ near(0.0, 1e-12)
+ );
+ assert_that!(
+ tdigest.quantile(5.0 / 30.0).unwrap().unwrap(),
+ near(10.0, 1e-12)
+ );
}
#[test]
@@ -737,7 +795,7 @@ fn test_quantile_handles_two_sample_last_centroid() {
let mut tdigest =
deserialize_with_centroids(100, 0.0, 100.0, &[(0.0, 1), (50.0, 1),
(90.0, 2)]);
- assert_eq!(tdigest.quantile(0.75), Some(100.0));
+ assert_eq!(tdigest.quantile(0.75).unwrap(), Some(100.0));
}
#[test]
@@ -757,10 +815,13 @@ fn
test_single_centroid_preserves_stored_tail_information() {
// Repeated queries agree before and after creating a mutable view.
for _ in 0..2 {
for (&point, &rank) in points.iter().zip(&expected) {
- assert_eq!(digest.rank(point), Some(rank));
- assert_eq!(frozen.rank(point), Some(rank));
+ assert_eq!(digest.rank(point).unwrap(), Some(rank));
+ assert_eq!(frozen.rank(point).unwrap(), Some(rank));
}
- assert_eq!(&digest.cdf(&points).unwrap()[..points.len()], &expected);
+ assert_eq!(
+ &digest.cdf(&points).unwrap().unwrap()[..points.len()],
+ &expected
+ );
}
}
@@ -786,14 +847,14 @@ fn
test_rank_tails_stay_monotonic_after_updating_weighted_centroids() {
// The new unit-weight tails lie inside the retained extrema. Their
half-sample
// centers leave no mass for interpolation between min/first or
last/max.
for (&point, &expected) in points.iter().zip(&expected) {
- assert_eq!(digest.rank(point), Some(expected));
- assert_eq!(frozen.rank(point), Some(expected));
+ assert_eq!(digest.rank(point).unwrap(), Some(expected));
+ assert_eq!(frozen.rank(point).unwrap(), Some(expected));
}
- let cdf = digest.cdf(&points).unwrap();
- assert_eq!(cdf, frozen.cdf(&points).unwrap());
+ let cdf = digest.cdf(&points).unwrap().unwrap();
+ assert_eq!(cdf, frozen.cdf(&points).unwrap().unwrap());
assert!(cdf.is_sorted(), "CDF at {points:?}: {cdf:?}");
- let pmf = digest.pmf(&points).unwrap();
- assert_eq!(pmf, frozen.pmf(&points).unwrap());
+ let pmf = digest.pmf(&points).unwrap().unwrap();
+ assert_eq!(pmf, frozen.pmf(&points).unwrap().unwrap());
assert!(pmf.iter().all(|&mass| mass >= 0.), "PMF: {pmf:?}");
assert_that!(pmf.iter().sum::<f64>(), near(1., 1e-12));
}
@@ -992,11 +1053,11 @@ fn
test_signed_zero_and_symmetric_centroids_survive_interpolation() {
let mut digest =
deserialize_with_centroids(100, -1., 1., &[(-1., 1), (-0., 2), (0.,
2), (1., 1)]);
- assert_eq!(digest.rank(-0.), Some(0.5));
- assert_eq!(digest.rank(0.), Some(0.5));
+ assert_eq!(digest.rank(-0.).unwrap(), Some(0.5));
+ assert_eq!(digest.rank(0.).unwrap(), Some(0.5));
assert_quantile_queries(&digest, &[1. / 3., 0.5, 2. / 3.], &[0., 0., 0.]);
- assert_eq!(digest.cdf(&[-0.]), Some(vec![0.5, 1.]));
- assert_eq!(digest.pmf(&[0.]), Some(vec![0.5, 0.5]));
+ assert_eq!(digest.cdf(&[-0.]).unwrap(), Some(vec![0.5, 1.]));
+ assert_eq!(digest.pmf(&[0.]).unwrap(), Some(vec![0.5, 0.5]));
}
#[test]
@@ -1015,11 +1076,14 @@ fn
test_batch_quantiles_match_scalar_queries_in_input_order() {
] {
let expected = ranks
.iter()
- .map(|&rank| frozen.quantile(rank).unwrap())
+ .map(|&rank| frozen.quantile(rank).unwrap().unwrap())
.collect::<Vec<_>>();
// Each mutable batch starts with the original buffered values still
pending.
- assert_eq!(tdigest.clone().quantiles(&ranks), Some(expected.clone()));
- assert_eq!(frozen.quantiles(&ranks), Some(expected));
+ assert_eq!(
+ tdigest.clone().quantiles(&ranks).unwrap(),
+ Some(expected.clone())
+ );
+ assert_eq!(frozen.quantiles(&ranks).unwrap(), Some(expected));
}
}
@@ -1109,9 +1173,11 @@ fn
test_quantiles_stay_within_extrema_at_large_total_weights() {
let frozen = tdigest.clone().freeze();
let ranks = [0.0, 0.5, 1.0_f64.next_down(), 1.0, 1.0];
for quantiles in [
- tdigest.clone().quantiles(&ranks).unwrap(),
- frozen.quantiles(&ranks).unwrap(),
- ranks.map(|rank| frozen.quantile(rank).unwrap()).to_vec(),
+ tdigest.clone().quantiles(&ranks).unwrap().unwrap(),
+ frozen.quantiles(&ranks).unwrap().unwrap(),
+ ranks
+ .map(|rank| frozen.quantile(rank).unwrap().unwrap())
+ .to_vec(),
] {
assert_eq!(quantiles[0], 0.0);
assert_eq!(&quantiles[3..], &[100.0, 100.0]);
@@ -1144,7 +1210,7 @@ fn test_queries_stay_ordered_at_large_centroid_centers() {
1.0_f64.next_down(),
1.,
];
- let quantiles = digest.quantiles(&ranks).unwrap();
+ let quantiles = digest.quantiles(&ranks).unwrap().unwrap();
assert_eq!(quantiles[0], 0.);
assert_eq!(quantiles[5], 100.);
assert!(
@@ -1153,7 +1219,10 @@ fn test_queries_stay_ordered_at_large_centroid_centers()
{
);
assert!(quantiles.iter().all(|value|
(0.0..=100.0).contains(value)));
- let cdf = digest.cdf(&[0., 5., 10., 50., 90., 95., 100.]).unwrap();
+ let cdf = digest
+ .cdf(&[0., 5., 10., 50., 90., 95., 100.])
+ .unwrap()
+ .unwrap();
assert!(cdf.is_sorted(), "centroids {centroids:?}: {cdf:?}");
assert!(cdf.iter().all(|rank| (0.0..=1.0).contains(rank)));
}
@@ -1176,7 +1245,7 @@ fn
test_quantile_right_tail_uses_the_same_center_as_rank() {
);
// These ranks land exactly on the rounded mass N - last_weight/2.
Quantile must
// return the centroid mean there, before interpolating toward the
stored maximum.
- assert_eq!(digest.rank(90.), Some(rank));
+ assert_eq!(digest.rank(90.).unwrap(), Some(rank));
assert_quantile_queries(&digest, &[rank, rank, 1.], &[90., 90., 100.]);
}
@@ -1186,7 +1255,7 @@ fn
test_quantile_right_tail_uses_the_same_center_as_rank() {
let digest = deserialize_with_centroids(100, 0., 100., &[(0., total_weight
- 3), (90., 3)]);
let rank = 1. - f64::EPSILON;
let ranks = [rank.next_down(), rank, rank.next_up(), 1.];
- let quantiles = digest.clone().quantiles(&ranks).unwrap();
+ let quantiles = digest.clone().quantiles(&ranks).unwrap().unwrap();
assert!(quantiles.is_sorted());
assert!(quantiles.iter().all(|value| (0.0..=100.0).contains(value)));
assert_eq!(quantiles[3], 100.);
@@ -1227,10 +1296,17 @@ fn
test_rank_max_uses_the_same_rounded_mass_as_last_centroid() {
deserialize_with_centroids(100, 0., 100., &[(0., total_weight -
1), (90., 1)]);
let frozen = digest.clone().freeze();
let points = [90., 90_f64.next_up(), 95., 100_f64.next_down(), 100.];
- let cdf = digest.cdf(&points).unwrap();
+ let cdf = digest.cdf(&points).unwrap().unwrap();
assert_eq!(&cdf[..points.len()], &[expected; 5]);
- assert_eq!(cdf, frozen.cdf(&points).unwrap());
- assert!(frozen.pmf(&points).unwrap().iter().all(|&mass| mass >= 0.));
+ assert_eq!(cdf, frozen.cdf(&points).unwrap().unwrap());
+ assert!(
+ frozen
+ .pmf(&points)
+ .unwrap()
+ .unwrap()
+ .iter()
+ .all(|&mass| mass >= 0.)
+ );
}
}
@@ -1238,20 +1314,28 @@ fn
test_rank_max_uses_the_same_rounded_mass_as_last_centroid() {
fn test_rank_stays_monotonic_at_the_right_tail_boundary() {
let digest = deserialize_with_centroids(100, 0., 300., &[(0., 1), (100.,
29)]).freeze();
let points = [100., 100_f64.next_up(), 150., 300.];
- let cdf = digest.cdf(&points).unwrap();
+ let cdf = digest.cdf(&points).unwrap().unwrap();
// The last center is at weight 1 + 29/2 = 15.5. Entering the tail must
not lower its rank.
assert_eq!(cdf[0], 15.5 / 30.);
assert!(cdf.is_sorted(), "CDF at {points:?}: {cdf:?}");
}
#[test]
-fn test_batch_quantiles_reject_invalid_ranks() {
- for values in [&[][..], &[1.0, 2.0, 3.0][..]] {
+fn test_invalid_queries_return_errors_before_checking_emptiness() {
+ for n in [0, 1, 1_000] {
let mut tdigest = TDigestMut::default();
- for &value in values {
- tdigest.update(value);
+ for value in 0..n {
+ tdigest.update(f64::from(value));
}
let frozen = tdigest.clone().freeze();
+ assert_eq!(
+ tdigest.rank(f64::NAN).unwrap_err().kind(),
+ ErrorKind::InvalidArgument
+ );
+ assert_eq!(
+ frozen.rank(f64::NAN).unwrap_err().kind(),
+ ErrorKind::InvalidArgument
+ );
for invalid in [
-f64::EPSILON,
1.0 + f64::EPSILON,
@@ -1260,24 +1344,77 @@ fn test_batch_quantiles_reject_invalid_ranks() {
f64::INFINITY,
] {
let ranks = [0.5, invalid];
- assert!(catch_unwind(AssertUnwindSafe(||
tdigest.quantiles(&ranks))).is_err());
- assert!(catch_unwind(|| frozen.quantiles(&ranks)).is_err());
+ for error in [
+ tdigest.quantile(invalid).unwrap_err(),
+ frozen.quantile(invalid).unwrap_err(),
+ tdigest.quantiles(&ranks).unwrap_err(),
+ frozen.quantiles(&ranks).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
+ }
+ for split_points in [
+ &[f64::NAN][..],
+ &[0.0, f64::NAN],
+ &[f64::NAN, 0.0],
+ &[1.0, 1.0],
+ &[2.0, 1.0],
+ &[-0.0, 0.0],
+ &[f64::INFINITY, f64::INFINITY],
+ ] {
+ for error in [
+ tdigest.cdf(split_points).unwrap_err(),
+ frozen.cdf(split_points).unwrap_err(),
+ tdigest.pmf(split_points).unwrap_err(),
+ frozen.pmf(split_points).unwrap_err(),
+ ] {
+ assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+ }
}
}
}
+#[test]
+fn test_infinite_values_are_valid_rank_and_split_point_arguments() {
+ for n in [0, 1, 1_000] {
+ let mut tdigest = TDigestMut::default();
+ for value in 0..n {
+ tdigest.update(f64::from(value));
+ }
+ let frozen = tdigest.clone().freeze();
+ for (value, rank) in [(f64::NEG_INFINITY, 0.0), (f64::INFINITY, 1.0)] {
+ let expected = (n > 0).then_some(rank);
+ assert_eq!(tdigest.rank(value).unwrap(), expected);
+ assert_eq!(frozen.rank(value).unwrap(), expected);
+ }
+ let split_points = [f64::NEG_INFINITY, f64::INFINITY];
+ let cdf = (n > 0).then(|| vec![0.0, 1.0, 1.0]);
+ let pmf = (n > 0).then(|| vec![0.0, 1.0, 0.0]);
+ assert_eq!(tdigest.cdf(&split_points).unwrap(), cdf);
+ assert_eq!(frozen.cdf(&split_points).unwrap(), cdf);
+ assert_eq!(tdigest.pmf(&split_points).unwrap(), pmf);
+ assert_eq!(frozen.pmf(&split_points).unwrap(), pmf);
+ }
+}
+
#[test]
fn test_rank_left_tail_is_a_fraction_of_the_total_weight() {
let mut tdigest =
deserialize_with_centroids(100, 0.0, 100.0, &[(10.0, 10), (50.0, 10),
(90.0, 10)]);
- assert_that!(tdigest.rank(5.0).unwrap(), near(0.1, 1e-12));
- assert_that!(tdigest.rank(10.0).unwrap(), near(5.0 / 30.0, 1e-12));
+ assert_that!(tdigest.rank(5.0).unwrap().unwrap(), near(0.1, 1e-12));
+ assert_that!(
+ tdigest.rank(10.0).unwrap().unwrap(),
+ near(5.0 / 30.0, 1e-12)
+ );
// The right tail is the mirror image and pins the scale the left tail
must match.
- assert_that!(tdigest.rank(95.0).unwrap(), near(0.9, 1e-12));
- assert_that!(tdigest.rank(90.0).unwrap(), near(25.0 / 30.0, 1e-12));
+ assert_that!(tdigest.rank(95.0).unwrap().unwrap(), near(0.9, 1e-12));
+ assert_that!(
+ tdigest.rank(90.0).unwrap().unwrap(),
+ near(25.0 / 30.0, 1e-12)
+ );
- let pmf = tdigest.pmf(&[5.0, 95.0]).unwrap();
+ let pmf = tdigest.pmf(&[5.0, 95.0]).unwrap().unwrap();
assert_that!(pmf[0], near(0.1, 1e-12));
assert_that!(pmf[1], near(0.8, 1e-12));
assert_that!(pmf[2], near(0.1, 1e-12));
@@ -1295,20 +1432,20 @@ fn test_merge_preserves_min_max_from_other() {
empty.merge(&other);
assert_eq!(empty.min_value(), Some(0.0));
assert_eq!(empty.max_value(), Some(100.0));
- assert_eq!(empty.quantile(0.0), Some(0.0));
- assert_eq!(empty.quantile(1.0), Some(100.0));
- assert_eq!(empty.rank(0.0), Some(0.5 / 30.0));
- assert_eq!(empty.rank(100.0), Some(1.0 - 0.5 / 30.0));
- assert_eq!(empty.rank(-1.0), Some(0.0));
- assert_eq!(empty.rank(101.0), Some(1.0));
+ assert_eq!(empty.quantile(0.0).unwrap(), Some(0.0));
+ assert_eq!(empty.quantile(1.0).unwrap(), Some(100.0));
+ assert_eq!(empty.rank(0.0).unwrap(), Some(0.5 / 30.0));
+ assert_eq!(empty.rank(100.0).unwrap(), Some(1.0 - 0.5 / 30.0));
+ assert_eq!(empty.rank(-1.0).unwrap(), Some(0.0));
+ assert_eq!(empty.rank(101.0).unwrap(), Some(1.0));
let mut left = deserialize_with_centroids(100, 5.0, 40.0, &[(10.0, 4),
(20.0, 4), (30.0, 4)]);
let right = deserialize_with_centroids(100, -10.0, 80.0, &[(0.0, 4),
(40.0, 4), (70.0, 4)]);
left.merge(&right);
assert_eq!(left.min_value(), Some(-10.0));
assert_eq!(left.max_value(), Some(80.0));
- assert_eq!(left.quantile(0.0), Some(-10.0));
- assert_eq!(left.quantile(1.0), Some(80.0));
+ assert_eq!(left.quantile(0.0).unwrap(), Some(-10.0));
+ assert_eq!(left.quantile(1.0).unwrap(), Some(80.0));
}
#[test]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]