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 22a766d refactor(req): model items as totally ordered values (#244)
22a766d is described below
commit 22a766d20e46b704e669d210af174791c25be3a2
Author: tison <[email protected]>
AuthorDate: Sat Aug 29 13:39:09 2026 +0800
refactor(req): model items as totally ordered values (#244)
---
CHANGELOG.md | 2 +-
datasketches/src/req/compactor.rs | 85 ++++---
datasketches/src/req/iter.rs | 7 +-
datasketches/src/req/mod.rs | 43 +++-
datasketches/src/req/sketch.rs | 94 +++++---
datasketches/src/req/sorted_view.rs | 73 +++---
datasketches/src/req/value.rs | 246 +++++++++------------
tests-integration/tests/req_test/accuracy.rs | 7 +-
tests-integration/tests/req_test/bounds.rs | 31 +--
tests-integration/tests/req_test/core.rs | 134 ++++++-----
.../tests/req_test/{main.rs => generic.rs} | 28 ++-
tests-integration/tests/req_test/main.rs | 14 ++
tests-integration/tests/req_test/merge.rs | 57 ++---
tests-integration/tests/req_test/property.rs | 29 +--
tests-integration/tests/req_test/query.rs | 71 +++---
.../tests/req_test/sorted_view_api.rs | 76 ++-----
tests-integration/tests/req_test/structure.rs | 19 +-
tests-integration/tests/serde_tests/req.rs | 96 ++++----
18 files changed, 567 insertions(+), 545 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02ffab6..67d8965 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,7 @@ All significant changes to this project will be documented in
this file.
### New features
-* Add Relative Error Quantiles (REQ) sketches behind the `req` feature,
including configurable high- or low-rank accuracy, rank, quantile, PMF, and CDF
queries, merging, and C++/Java-compatible serialization.
+* Add Relative Error Quantiles (REQ) sketches behind the `req` feature,
including configurable high- or low-rank accuracy, rank, quantile, PMF, and CDF
queries, merging, totally ordered custom item types, the `ReqFloat` adapter for
non-NaN floating-point values, and C++/Java-compatible serialization.
### Performance improvements
diff --git a/datasketches/src/req/compactor.rs
b/datasketches/src/req/compactor.rs
index 07a6f38..e4835dc 100644
--- a/datasketches/src/req/compactor.rs
+++ b/datasketches/src/req/compactor.rs
@@ -28,21 +28,6 @@ use crate::req::nearest_even_section_size;
use crate::req::serialization::validate_compactor_state;
use crate::req::value::ReqValue;
-fn normalized_sort_state<T: ReqValue>(items: &[T], claimed_sorted: bool) ->
Result<bool, Error> {
- let mut previous: Option<&T> = None;
- let mut sorted = claimed_sorted;
- for item in items {
- if item.is_nan() {
- return Err(Error::deserial("REQ compactor contains a NaN item"));
- }
- if sorted && previous.is_some_and(|previous|
previous.compare(item).is_gt()) {
- sorted = false;
- }
- previous = Some(item);
- }
- Ok(sorted)
-}
-
/// A compactor maintains items at a specific level of the REQ sketch.
///
/// When the compactor reaches its nominal capacity, it performs compaction
@@ -75,7 +60,7 @@ pub(super) struct Compactor<T> {
impl<T> Compactor<T>
where
- T: Clone + ReqValue,
+ T: Clone + Ord,
{
/// Creates a new compactor for the given level.
///
@@ -140,7 +125,7 @@ where
self.merge_sorted(&other.items);
} else {
let mut other_items = other.items.clone();
- other_items.sort_unstable_by(|a, b| a.compare(b));
+ other_items.sort_unstable();
self.merge_sorted(&other_items);
}
}
@@ -158,17 +143,14 @@ where
pub(super) fn count_below(&self, item: &T, inclusive: bool) -> usize {
if self.is_sorted {
if inclusive {
- self.items.partition_point(|x| x.compare(item).is_le())
+ self.items.partition_point(|x| x <= item)
} else {
- self.items.partition_point(|x| x.compare(item).is_lt())
+ self.items.partition_point(|x| x < item)
}
} else {
self.items
.iter()
- .filter(|x| {
- let ord = x.compare(item);
- if inclusive { ord.is_le() } else { ord.is_lt() }
- })
+ .filter(|x| if inclusive { *x <= item } else { *x < item })
.count()
}
}
@@ -201,7 +183,7 @@ where
// Two-pointer merge into scratch buffer
while i < a.len() && j < b.len() {
- if a[i].compare(&b[j]).is_le() {
+ if a[i] <= b[j] {
self.scratch_buffer.push(a[i].clone());
i += 1;
} else {
@@ -229,7 +211,7 @@ where
pub(super) fn sort(&mut self) {
if !self.is_sorted {
// Use unstable sort for better performance (stable not needed for
REQ sketch)
- self.items.sort_unstable_by(|a, b| a.compare(b));
+ self.items.sort_unstable();
self.is_sorted = true;
}
}
@@ -367,7 +349,10 @@ where
}
/// Serialize this compactor (preamble + items) into the byte buffer.
- pub(super) fn serialize_into(&self, bytes: &mut crate::codec::SketchBytes)
{
+ pub(super) fn serialize_into(&self, bytes: &mut crate::codec::SketchBytes)
+ where
+ T: ReqValue,
+ {
bytes.write_u64_le(self.state);
bytes.write_f32_le(self.section_size_raw);
bytes.write_u8(self.lg_weight);
@@ -386,7 +371,10 @@ where
expected_lg_weight: u8,
rank_accuracy: RankAccuracy,
sorted: bool,
- ) -> Result<Self, crate::error::Error> {
+ ) -> Result<Self, Error>
+ where
+ T: ReqValue,
+ {
use crate::codec::assert::insufficient_data;
let state = cursor
.read_u64_le()
@@ -425,7 +413,7 @@ where
for _ in 0..num_items {
items.push(T::deserialize_value(cursor)?);
}
- let sorted = normalized_sort_state(&items, sorted)?;
+ let sorted = sorted && items.is_sorted();
Ok(Compactor::from_serialized_state(
lg_weight,
@@ -442,23 +430,19 @@ where
///
/// The wire format omits the compactor preamble for tiny sketches (n ≤
4); this
/// helper synthesises a fresh compactor and seeds it with the
deserialized items.
- /// `is_sorted` is taken verbatim from the wire — both C++ and Java
produce valid
- /// sketches but disagree on this flag for n=1 (C++ sets true, Java sets
false),
- /// so faithful round-trip requires preserving whatever the input said.
+ /// A false wire flag stays false for byte-stable C++/Java round trips; a
true flag
+ /// is cleared if the items are not actually sorted.
pub(super) fn raw_items_compactor(
k: u16,
rank_accuracy: RankAccuracy,
items: Vec<T>,
is_sorted: bool,
- ) -> Result<Self, Error> {
- let is_sorted = normalized_sort_state(&items, is_sorted)?;
+ ) -> Self {
+ let is_sorted = is_sorted && items.is_sorted();
let mut c = Self::new(0, k, rank_accuracy);
- for item in items {
- c.append(item);
- }
- // append() may have flipped is_sorted off; restore the wire flag
verbatim.
+ c.items = items;
c.is_sorted = is_sorted;
- Ok(c)
+ c
}
/// Reconstruct a Compactor from deserialized state.
@@ -494,7 +478,7 @@ where
#[cfg(test)]
impl<T> Compactor<T>
where
- T: Clone + ReqValue,
+ T: Clone + Ord,
{
/// Returns the level (log weight) of this compactor. Test-only accessor.
pub(super) fn lg_weight(&self) -> u8 {
@@ -513,6 +497,7 @@ mod tests {
use googletest::prelude::ge;
use super::*;
+ use crate::req::ReqFloat;
#[test]
fn test_new_compactor() {
@@ -576,9 +561,9 @@ mod tests {
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
- let mut c: Compactor<f32> = Compactor::new(0, 12,
RankAccuracy::HighRank);
+ let mut c: Compactor<ReqFloat<f32>> = Compactor::new(0, 12,
RankAccuracy::HighRank);
for i in 0..30 {
- c.append(i as f32);
+ c.append(ReqFloat::<f32>::new(i as f32).unwrap());
}
c.sort();
@@ -587,14 +572,20 @@ mod tests {
let raw = bytes.into_bytes();
let mut cursor = SketchSlice::new(&raw);
- let c2 = Compactor::<f32>::deserialize(&mut cursor, 12, 0,
RankAccuracy::HighRank, true)
- .unwrap();
+ let c2 = Compactor::<ReqFloat<f32>>::deserialize(
+ &mut cursor,
+ 12,
+ 0,
+ RankAccuracy::HighRank,
+ true,
+ )
+ .unwrap();
assert_eq!(c.num_items(), c2.num_items());
assert_eq!(c.lg_weight(), c2.lg_weight());
assert_eq!(c.state(), c2.state());
- let xs: Vec<f32> = c.iter().copied().collect();
- let ys: Vec<f32> = c2.iter().copied().collect();
+ let xs: Vec<ReqFloat<f32>> = c.iter().copied().collect();
+ let ys: Vec<ReqFloat<f32>> = c2.iter().copied().collect();
assert_eq!(xs, ys);
}
@@ -615,8 +606,8 @@ mod tests {
// - state=0xFFFF >= (1<<11)=2048 ✓ → num_sections=24, ssr≈4.24
// - state=0xFFFF >= (1<<23)=8388608 ✗ → stop
// Expected: num_sections == 24 with the fix; == 6 with only one call.
- let mut a: Compactor<f64> = Compactor::new(0, 12,
RankAccuracy::HighRank);
- let mut b: Compactor<f64> = Compactor::new(0, 12,
RankAccuracy::HighRank);
+ let mut a: Compactor<i32> = Compactor::new(0, 12,
RankAccuracy::HighRank);
+ let mut b: Compactor<i32> = Compactor::new(0, 12,
RankAccuracy::HighRank);
b.state = 0xFFFF;
assert_eq!(a.num_sections, 3, "default num_sections sanity");
diff --git a/datasketches/src/req/iter.rs b/datasketches/src/req/iter.rs
index a35aadb..ba5c0f3 100644
--- a/datasketches/src/req/iter.rs
+++ b/datasketches/src/req/iter.rs
@@ -18,7 +18,6 @@
//! Iterator implementations for REQ sketch inspection.
use crate::req::compactor::Compactor;
-use crate::req::value::ReqValue;
/// Iterator over (item, weight) pairs in a REQ sketch.
///
@@ -26,14 +25,14 @@ use crate::req::value::ReqValue;
/// which depend on the level of the compactor they're stored in.
///
/// Zero-allocation implementation that works directly with slices.
-pub struct ReqSketchIterator<'a, T: ReqValue> {
+pub struct ReqSketchIterator<'a, T> {
compactors: &'a [Compactor<T>],
current_level: usize,
current_level_iter: Option<std::slice::Iter<'a, T>>,
current_weight: u64,
}
-impl<'a, T: ReqValue> ReqSketchIterator<'a, T> {
+impl<'a, T: Clone + Ord> ReqSketchIterator<'a, T> {
/// Creates a new iterator over the compactors.
pub(super) fn new(compactors: &'a [Compactor<T>]) -> Self {
let mut iter = Self {
@@ -65,7 +64,7 @@ impl<'a, T: ReqValue> ReqSketchIterator<'a, T> {
}
}
-impl<T: ReqValue> Iterator for ReqSketchIterator<'_, T> {
+impl<T: Clone + Ord> Iterator for ReqSketchIterator<'_, T> {
type Item = (T, u64);
fn next(&mut self) -> Option<Self::Item> {
diff --git a/datasketches/src/req/mod.rs b/datasketches/src/req/mod.rs
index 37f26ca..8714f5e 100644
--- a/datasketches/src/req/mod.rs
+++ b/datasketches/src/req/mod.rs
@@ -17,11 +17,37 @@
//! Relative Error Quantiles (REQ) sketch.
//!
-//! Provides approximate quantile estimation with relative error guarantees,
especially
-//! useful for streaming scenarios needing bounded memory. Based on the paper
-//! [Relative Error Streaming Quantiles](https://arxiv.org/abs/2004.01668) by
Cormode,
-//! Karnin, Liberty, Thaler and Veselý, and on the Apache DataSketches C++
reference
-//! implementation.
+//! [`ReqSketch`] provides bounded-memory rank, quantile, PMF, and CDF
estimates with
+//! configurable relative accuracy at either end of the rank domain. It is
based on
+//! [Relative Error Streaming Quantiles](https://arxiv.org/abs/2004.01668) and
the Apache
+//! DataSketches C++ implementation.
+//!
+//! # Item ordering
+//!
+//! The REQ paper defines input items as coming from a totally ordered
universe. Accordingly,
+//! sketch items must implement [`Ord`]. Rust's `f32` and `f64` do not
implement `Ord` because NaN
+//! is unordered; wrap floating-point items in [`ReqFloat`], whose constructor
rejects NaN. Signed
+//! zeros compare equal and infinities retain their usual numerical order.
+//!
+//! Custom item types need only [`Clone`] and [`Ord`] for in-memory use.
Serialization additionally
+//! requires [`ReqValue`].
+//!
+//! # Example
+//!
+//! ```
+//! use datasketches::req::ReqFloat;
+//! use datasketches::req::ReqSketch;
+//! use datasketches::req::SearchCriteria;
+//!
+//! let mut sketch = ReqSketch::default();
+//! for value in [1.0, 2.0, 3.0] {
+//! sketch.update(ReqFloat::<f64>::new(value)?);
+//! }
+//!
+//! let median = sketch.quantile(0.5, SearchCriteria::Inclusive)?;
+//! assert_eq!(median.into_inner(), 2.0);
+//! # Ok::<(), datasketches::error::Error>(())
+//! ```
mod compactor;
mod iter;
@@ -33,6 +59,7 @@ mod value;
pub use self::iter::ReqSketchIterator;
pub use self::sketch::ReqSketch;
pub use self::sorted_view::SortedView;
+pub use self::value::ReqFloat;
pub use self::value::ReqValue;
/// Default value of `k` if not specified. Roughly 1% relative error at 95%
confidence.
@@ -52,13 +79,13 @@ pub enum RankAccuracy {
LowRank,
}
-/// Whether queries include the weight of the search item.
+/// Selects the rank definition used by rank, quantile, PMF, and CDF queries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SearchCriteria {
- /// Include the weight of the search item in the result.
+ /// Define rank as the fraction of values less than or equal to the
boundary.
#[default]
Inclusive,
- /// Exclude the weight of the search item from the result.
+ /// Define rank as the fraction of values strictly less than the boundary.
Exclusive,
}
diff --git a/datasketches/src/req/sketch.rs b/datasketches/src/req/sketch.rs
index 16fe895..1a2e263 100644
--- a/datasketches/src/req/sketch.rs
+++ b/datasketches/src/req/sketch.rs
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-//! REQ sketch — generic over `T: ReqValue`.
+//! Generic REQ sketch implementation.
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
@@ -45,9 +45,9 @@ use crate::req::value::ReqValue;
/// A Relative Error Quantiles sketch for approximate quantile estimation.
///
-/// See the [module-level documentation](super) for background.
+/// See the [module level documentation](super) for item ordering and
floating-point requirements.
#[derive(Debug, Clone)]
-pub struct ReqSketch<T: ReqValue> {
+pub struct ReqSketch<T> {
k: u16,
rank_accuracy: RankAccuracy,
n: u64,
@@ -59,14 +59,22 @@ pub struct ReqSketch<T: ReqValue> {
max_item: Option<T>,
}
-impl<T: ReqValue> Default for ReqSketch<T> {
+impl<T> Default for ReqSketch<T>
+where
+ T: Clone + Ord,
+{
fn default() -> Self {
Self::make(DEFAULT_K, RankAccuracy::HighRank)
}
}
-impl<T: ReqValue> ReqSketch<T> {
- /// Creates a new sketch with the given `k` and rank accuracy.
+impl<T> ReqSketch<T>
+where
+ T: Clone + Ord,
+{
+ /// Creates a sketch with the given `k` and rank-accuracy mode.
+ ///
+ /// Larger `k` improves accuracy at the cost of retained memory.
///
/// # Errors
///
@@ -93,7 +101,7 @@ impl<T: ReqValue> ReqSketch<T> {
self.rank_accuracy
}
- /// Returns the total number of items observed (matches C++ `get_n`).
+ /// Returns the total number of items observed.
pub fn n(&self) -> u64 {
self.n
}
@@ -124,21 +132,15 @@ impl<T: ReqValue> ReqSketch<T> {
}
/// Updates the sketch with a new item.
- ///
- /// NaN inputs are silently ignored for floating-point types, matching the
behavior
- /// of the Java reference implementation (`checkNaNUpdate`). This is
intentional.
pub fn update(&mut self, item: T) {
- if item.is_nan() {
- return;
- }
match &mut self.min_item {
None => self.min_item = Some(item.clone()),
- Some(cur) if item.compare(cur).is_lt() => *cur = item.clone(),
+ Some(cur) if item.cmp(cur).is_lt() => *cur = item.clone(),
_ => {}
}
match &mut self.max_item {
None => self.max_item = Some(item.clone()),
- Some(cur) if item.compare(cur).is_gt() => *cur = item.clone(),
+ Some(cur) if item.cmp(cur).is_gt() => *cur = item.clone(),
_ => {}
}
@@ -174,14 +176,12 @@ impl<T: ReqValue> ReqSketch<T> {
/// [`SortedView::rank`] on [`Self::sorted_view`].
///
/// # Errors
- /// Returns an error if the sketch is empty or `item` is NaN.
+ ///
+ /// Returns an error if the sketch is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
if self.is_empty() {
return Err(Error::invalid_argument("sketch is empty"));
}
- if item.is_nan() {
- return Err(Error::invalid_argument("query item is NaN"));
- }
let inclusive = matches!(criteria, SearchCriteria::Inclusive);
let weight: u64 = self
.compactors
@@ -195,6 +195,10 @@ impl<T: ReqValue> ReqSketch<T> {
///
/// Builds a transient [`SortedView`] internally. For repeated quantile
/// queries, take one snapshot with [`Self::sorted_view`] and query it.
+ ///
+ /// # 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"));
@@ -210,6 +214,10 @@ impl<T: ReqValue> ReqSketch<T> {
/// Returns approximate quantiles for the given normalized ranks.
///
/// 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"));
@@ -227,6 +235,10 @@ impl<T: ReqValue> ReqSketch<T> {
}
/// Returns the Probability Mass Function over the given split points.
+ ///
+ /// # 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"));
@@ -235,6 +247,10 @@ impl<T: ReqValue> ReqSketch<T> {
}
/// Returns the Cumulative Distribution Function over the given split
points.
+ ///
+ /// # 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"));
@@ -273,19 +289,21 @@ impl<T: ReqValue> ReqSketch<T> {
/// # Examples
///
/// ```
+ /// use datasketches::req::ReqFloat;
/// use datasketches::req::ReqSketch;
///
/// let mut first = ReqSketch::default();
- /// first.update(1.0_f64);
+ /// first.update(ReqFloat::<f64>::new(1.0)?);
///
/// let mut second = ReqSketch::default();
- /// second.update(2.0_f64);
+ /// second.update(ReqFloat::<f64>::new(2.0)?);
///
/// let mut combined = ReqSketch::default();
/// combined.merge(&first).unwrap();
/// combined.merge(&second).unwrap();
///
/// assert_eq!(combined.n(), 2);
+ /// # Ok::<(), datasketches::error::Error>(())
/// ```
pub fn merge(&mut self, other: &Self) -> Result<(), Error> {
if self.rank_accuracy != other.rank_accuracy {
@@ -303,14 +321,14 @@ impl<T: ReqValue> ReqSketch<T> {
if let Some(m) = &other.min_item {
match &self.min_item {
None => self.min_item = Some(m.clone()),
- Some(cur) if m.compare(cur).is_lt() => self.min_item =
Some(m.clone()),
+ Some(cur) if m.cmp(cur).is_lt() => self.min_item =
Some(m.clone()),
_ => {}
}
}
if let Some(m) = &other.max_item {
match &self.max_item {
None => self.max_item = Some(m.clone()),
- Some(cur) if m.compare(cur).is_gt() => self.max_item =
Some(m.clone()),
+ Some(cur) if m.cmp(cur).is_gt() => self.max_item =
Some(m.clone()),
_ => {}
}
}
@@ -473,8 +491,11 @@ impl<T: ReqValue> ReqSketch<T> {
self.n <= RAW_ITEMS_THRESHOLD && self.compactors.len() == 1
}
- /// Number of bytes required to serialize the sketch.
- pub fn serialized_size_bytes(&self) -> usize {
+ /// Returns the number of bytes required to serialize the sketch.
+ pub fn serialized_size_bytes(&self) -> usize
+ where
+ T: ReqValue,
+ {
// Fixed sketch preamble: 8 bytes (preamble_ints, serial_version,
family,
// flags, k(2), num_levels, num_raw_items).
let mut size = 8usize;
@@ -502,8 +523,11 @@ impl<T: ReqValue> ReqSketch<T> {
size
}
- /// Serialize the sketch into a `Vec<u8>` matching the C++/Java REQ wire
format.
- pub fn serialize(&self) -> Vec<u8> {
+ /// Serializes the sketch using the C++/Java-compatible REQ wire format.
+ pub fn serialize(&self) -> Vec<u8>
+ where
+ T: ReqValue,
+ {
let mut out = SketchBytes::with_capacity(self.serialized_size_bytes());
let preamble_ints = if self.is_estimation_mode() {
PREAMBLE_INTS_ESTIMATION
@@ -554,7 +578,10 @@ impl<T: ReqValue> ReqSketch<T> {
///
/// Returns an error if the input is truncated or contains an inconsistent
/// REQ serialized state.
- pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
+ pub fn deserialize(bytes: &[u8]) -> Result<Self, Error>
+ where
+ T: ReqValue,
+ {
let mut cursor = SketchSlice::new(bytes);
let preamble_ints = cursor
.read_u8()
@@ -646,10 +673,7 @@ impl<T: ReqValue> ReqSketch<T> {
max_item = Some(T::deserialize_value(&mut cursor)?);
let min = min_item.as_ref().unwrap();
let max = max_item.as_ref().unwrap();
- if min.is_nan() || max.is_nan() {
- return Err(Error::deserial("REQ sketch min or max item is
NaN"));
- }
- if min.compare(max).is_gt() {
+ if min > max {
return Err(Error::deserial(
"REQ sketch min item is greater than max item",
));
@@ -665,7 +689,7 @@ impl<T: ReqValue> ReqSketch<T> {
items.push(T::deserialize_value(&mut cursor)?);
}
let c =
- Compactor::<T>::raw_items_compactor(k, rank_accuracy, items,
is_level_zero_sorted)?;
+ Compactor::<T>::raw_items_compactor(k, rank_accuracy, items,
is_level_zero_sorted);
compactors.push(c);
} else {
for i in 0..num_levels {
@@ -685,10 +709,10 @@ impl<T: ReqValue> ReqSketch<T> {
let mut mn = first.clone();
let mut mx = first.clone();
for x in iter {
- if x.compare(&mn).is_lt() {
+ if x < &mn {
mn = x.clone();
}
- if x.compare(&mx).is_gt() {
+ if x > &mx {
mx = x.clone();
}
}
diff --git a/datasketches/src/req/sorted_view.rs
b/datasketches/src/req/sorted_view.rs
index 5c2d5a1..d1313ac 100644
--- a/datasketches/src/req/sorted_view.rs
+++ b/datasketches/src/req/sorted_view.rs
@@ -19,10 +19,8 @@
use crate::error::Error;
use crate::req::SearchCriteria;
-use crate::req::value::ReqValue;
-/// An owned, sorted snapshot of a [`ReqSketch`](crate::req::ReqSketch)'s
items with
-/// their cumulative weights.
+/// 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
@@ -42,7 +40,7 @@ pub struct SortedView<T> {
impl<T> SortedView<T>
where
- T: ReqValue,
+ T: Clone + Ord,
{
/// Creates a new sorted view from weighted items.
///
@@ -60,7 +58,7 @@ where
}
// Sort by item value - use unstable sort for better performance
- weighted_items.sort_unstable_by(|a, b| a.0.compare(&b.0));
+ weighted_items.sort_unstable_by(|a, b| a.0.cmp(&b.0));
let mut items: Vec<T> = Vec::with_capacity(weighted_items.len());
let mut cumulative_weights = Vec::with_capacity(weighted_items.len());
@@ -68,7 +66,7 @@ where
for (item, weight) in weighted_items {
if let Some(last) = items.last() {
- if matches!(last.compare(&item), std::cmp::Ordering::Equal) {
+ if last == &item {
cumulative_weight += weight;
let last_idx = cumulative_weights.len() - 1;
cumulative_weights[last_idx] = cumulative_weight;
@@ -102,27 +100,20 @@ where
self.total_weight
}
- /// Returns the approximate normalized rank of the given item in `[0.0,
1.0]`.
- ///
- /// # Arguments
- /// * `item` - The item to find the rank for
- /// * `criteria` - Whether to include the item's weight in the rank
+ /// Returns the approximate normalized rank of `item` in `[0.0, 1.0]`.
///
/// # Errors
- /// Returns an error if the view is empty or `item` is NaN.
+ ///
+ /// Returns an error if the view is empty.
pub fn rank(&self, item: &T, criteria: SearchCriteria) -> Result<f64,
Error> {
if self.is_empty() {
return Err(Error::invalid_argument("sketch is empty"));
}
- if item.is_nan() {
- return Err(Error::invalid_argument("query item is NaN"));
- }
-
match criteria {
SearchCriteria::Inclusive => {
// Find the last position where items[i] <= item
// partition_point finds first index where predicate is false
- let pos = self.items.partition_point(|x|
x.compare(item).is_le());
+ let pos = self.items.partition_point(|x| x <= item);
if pos == 0 {
Ok(0.0)
} else {
@@ -131,7 +122,7 @@ where
}
SearchCriteria::Exclusive => {
// Find the last position where items[i] < item
- let pos = self.items.partition_point(|x|
x.compare(item).is_lt());
+ let pos = self.items.partition_point(|x| x < item);
if pos == 0 {
Ok(0.0)
} else {
@@ -141,14 +132,11 @@ where
}
}
- /// Returns the approximate quantile for the given normalized rank.
+ /// Returns the approximate quantile at the given normalized rank.
///
- /// # Arguments
- /// * `rank` - A normalized rank in [0.0, 1.0]
- /// * `criteria` - Search criteria for quantile selection
+ /// # Errors
///
- /// # Returns
- /// The item at approximately the given rank
+ /// 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"));
@@ -199,14 +187,13 @@ where
Ok(self.items[index].clone())
}
- /// Returns the Probability Mass Function (PMF) for the given split points.
+ /// Returns the probability mass function (PMF) over the given split
points.
///
- /// # Arguments
- /// * `split_points` - Array of split points that divide the domain
- /// * `criteria` - Search criteria for boundary handling
+ /// The result contains one more value than `split_points`.
///
- /// # Returns
- /// Array of probabilities for each interval defined by the split points
+ /// # 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> {
if self.is_empty() {
return Err(Error::invalid_argument("sketch is empty"));
@@ -229,14 +216,13 @@ where
Ok(result)
}
- /// Returns the Cumulative Distribution Function (CDF) for the given split
points.
+ /// Returns the cumulative distribution function (CDF) over the given
split points.
///
- /// # Arguments
- /// * `split_points` - Array of split points that divide the domain
- /// * `criteria` - Search criteria for boundary handling
+ /// The result contains one more value than `split_points` and ends at
`1.0`.
+ ///
+ /// # Errors
///
- /// # Returns
- /// Array of cumulative probabilities at each split point
+ /// 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> {
if self.is_empty() {
return Err(Error::invalid_argument("sketch is empty"));
@@ -256,18 +242,11 @@ where
Ok(result)
}
- // Private helper methods
-
fn validate_split_points(&self, split_points: &[T]) -> Result<(), Error> {
- for (i, split_point) in split_points.iter().enumerate() {
- if split_point.is_nan() {
- return Err(Error::invalid_argument("Split points must not be
NaN"));
- }
- if i > 0 && split_points[i - 1].compare(split_point).is_ge() {
- return Err(Error::invalid_argument(
- "Split points must be unique and monotonically
increasing".to_string(),
- ));
- }
+ if split_points.windows(2).any(|pair| pair[0] >= pair[1]) {
+ return Err(Error::invalid_argument(
+ "Split points must be unique and monotonically
increasing".to_string(),
+ ));
}
Ok(())
}
diff --git a/datasketches/src/req/value.rs b/datasketches/src/req/value.rs
index a2c5ed4..8bd3f30 100644
--- a/datasketches/src/req/value.rs
+++ b/datasketches/src/req/value.rs
@@ -15,49 +15,126 @@
// specific language governing permissions and limitations
// under the License.
-//! Trait for types storable in a [`ReqSketch`](crate::req::ReqSketch).
+//! REQ item types and serialization.
use std::cmp::Ordering;
+use std::fmt;
use std::mem::size_of;
+use std::ops::Deref;
use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
use crate::error::Error;
-/// Trait for types that can be stored in a
[`ReqSketch`](crate::req::ReqSketch).
+/// A non-NaN floating-point adapter for [`ReqSketch`](crate::req::ReqSketch).
///
-/// Provides ordering and binary serialization compatible with the Apache
DataSketches
-/// REQ wire format used by the C++ and Java reference implementations.
-pub trait ReqValue: Sized + Clone + PartialOrd {
- /// Compares two values. See each implementation for its ordering
semantics.
- fn compare(&self, other: &Self) -> Ordering;
+/// REQ requires a totally ordered item domain, while primitive floats are
unordered in the
+/// presence of NaN. Construction therefore rejects NaN. Other values retain
their numerical
+/// order: signed zeros compare equal and infinities are allowed.
+///
+/// The inner float is available through [`into_inner`](Self::into_inner) or
immutable
+/// dereferencing.
+#[repr(transparent)]
+#[derive(Clone, Copy, PartialEq, PartialOrd)]
+pub struct ReqFloat<T>(T);
+
+impl<T> ReqFloat<T> {
+ /// Returns the wrapped floating-point value.
+ #[inline(always)]
+ pub fn into_inner(self) -> T {
+ self.0
+ }
+}
+
+impl<T> Deref for ReqFloat<T> {
+ type Target = T;
- /// Returns true if this value is the floating-point NaN sentinel.
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T: fmt::Debug> fmt::Debug for ReqFloat<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl<T: fmt::Display> fmt::Display for ReqFloat<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl ReqFloat<f32> {
+ /// Creates a non-NaN value.
+ ///
+ /// # Errors
///
- /// Default: false (integer types are never NaN). Float impls override
- /// to delegate to [`f32::is_nan`] / [`f64::is_nan`].
+ /// Returns an error if `value` is NaN.
#[inline(always)]
- fn is_nan(&self) -> bool {
- false
+ pub fn new(value: f32) -> Result<Self, Error> {
+ if value.is_nan() {
+ Err(Error::invalid_argument("REQ float must not be NaN"))
+ } else {
+ Ok(Self(value))
+ }
}
+}
+
+impl Eq for ReqFloat<f32> {}
- /// Number of bytes this value will occupy when serialized.
+impl Ord for ReqFloat<f32> {
+ #[inline(always)]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.partial_cmp(&other.0).unwrap()
+ }
+}
+
+impl ReqFloat<f64> {
+ /// Creates a non-NaN value.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `value` is NaN.
+ #[inline(always)]
+ pub fn new(value: f64) -> Result<Self, Error> {
+ if value.is_nan() {
+ Err(Error::invalid_argument("REQ float must not be NaN"))
+ } else {
+ Ok(Self(value))
+ }
+ }
+}
+
+impl Eq for ReqFloat<f64> {}
+
+impl Ord for ReqFloat<f64> {
+ #[inline(always)]
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.partial_cmp(&other.0).unwrap()
+ }
+}
+
+/// Binary serialization for REQ items.
+///
+/// This trait is not required for in-memory sketch operations. Implement it
only when a custom
+/// item type must be used with
[`ReqSketch::serialize`](crate::req::ReqSketch::serialize) and
+/// [`ReqSketch::deserialize`](crate::req::ReqSketch::deserialize). The
encoded form must preserve
+/// the item's ordering across a round trip.
+pub trait ReqValue: Sized {
+ /// Returns the serialized size of `item` in bytes.
fn serialize_size(item: &Self) -> usize;
- /// Serialize this value into the byte buffer.
+ /// Serializes this value into `bytes`.
fn serialize_value(&self, bytes: &mut SketchBytes);
- /// Deserialize a value from the byte cursor.
+ /// Deserializes one value from `cursor`.
fn deserialize_value(cursor: &mut SketchSlice<'_>) -> Result<Self, Error>;
}
impl ReqValue for i32 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.cmp(other)
- }
-
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
@@ -74,11 +151,6 @@ impl ReqValue for i32 {
}
impl ReqValue for i64 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.cmp(other)
- }
-
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
@@ -95,11 +167,6 @@ impl ReqValue for i64 {
}
impl ReqValue for u32 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.cmp(other)
- }
-
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
@@ -116,11 +183,6 @@ impl ReqValue for u32 {
}
impl ReqValue for u64 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.cmp(other)
- }
-
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
@@ -136,130 +198,36 @@ impl ReqValue for u64 {
}
}
-impl ReqValue for f32 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.partial_cmp(other).unwrap()
- }
-
- #[inline(always)]
- fn is_nan(&self) -> bool {
- f32::is_nan(*self)
- }
-
+impl ReqValue for ReqFloat<f32> {
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
fn serialize_value(&self, bytes: &mut SketchBytes) {
- bytes.write_f32_le(*self);
+ bytes.write_f32_le(self.0);
}
fn deserialize_value(cursor: &mut SketchSlice<'_>) -> Result<Self, Error> {
- cursor
+ let value = cursor
.read_f32_le()
- .map_err(insufficient_data("failed to read f32 from REQ sketch"))
+ .map_err(insufficient_data("failed to read f32 from REQ sketch"))?;
+ Self::new(value).map_err(|_| Error::deserial("REQ float must not be
NaN"))
}
}
-impl ReqValue for f64 {
- #[inline(always)]
- fn compare(&self, other: &Self) -> Ordering {
- self.partial_cmp(other).unwrap()
- }
-
- #[inline(always)]
- fn is_nan(&self) -> bool {
- f64::is_nan(*self)
- }
-
+impl ReqValue for ReqFloat<f64> {
fn serialize_size(_item: &Self) -> usize {
size_of::<Self>()
}
fn serialize_value(&self, bytes: &mut SketchBytes) {
- bytes.write_f64_le(*self);
+ bytes.write_f64_le(self.0);
}
fn deserialize_value(cursor: &mut SketchSlice<'_>) -> Result<Self, Error> {
- cursor
+ let value = cursor
.read_f64_le()
- .map_err(insufficient_data("failed to read f64 from REQ sketch"))
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn round_trip<T: ReqValue + PartialEq + std::fmt::Debug>(v: T) {
- let mut bytes = SketchBytes::with_capacity(T::serialize_size(&v));
- v.serialize_value(&mut bytes);
- let raw = bytes.into_bytes();
- assert_eq!(raw.len(), T::serialize_size(&v));
- let mut cursor = SketchSlice::new(&raw);
- let got = T::deserialize_value(&mut cursor).unwrap();
- assert_eq!(got, v);
- }
-
- #[test]
- fn round_trip_integers() {
- round_trip(0_i32);
- round_trip(i32::MIN);
- round_trip(i32::MAX);
- round_trip(0_i64);
- round_trip(i64::MIN);
- round_trip(i64::MAX);
- round_trip(0_u32);
- round_trip(u32::MAX);
- round_trip(0_u64);
- round_trip(u64::MAX);
- }
-
- #[test]
- fn round_trip_floats() {
- round_trip(0.0_f32);
- round_trip(-1.5_f32);
- round_trip(f32::MIN);
- round_trip(f32::MAX);
- round_trip(f32::INFINITY);
- round_trip(f32::NEG_INFINITY);
- round_trip(0.0_f64);
- round_trip(-1.5_f64);
- round_trip(f64::MIN);
- round_trip(f64::MAX);
- round_trip(f64::INFINITY);
- round_trip(f64::NEG_INFINITY);
- }
-
- #[test]
- fn compare_for_f32_uses_numeric_order() {
- assert_eq!(<f32 as ReqValue>::compare(&-0.0, &0.0), Ordering::Equal);
- assert_eq!(
- <f32 as ReqValue>::compare(&f32::NEG_INFINITY, &f32::INFINITY),
- Ordering::Less
- );
- }
-
- #[test]
- fn compare_for_f64_uses_numeric_order() {
- assert_eq!(<f64 as ReqValue>::compare(&-0.0, &0.0), Ordering::Equal);
- assert_eq!(
- <f64 as ReqValue>::compare(&f64::NEG_INFINITY, &f64::INFINITY),
- Ordering::Less
- );
- }
-
- #[test]
- #[should_panic]
- fn compare_for_floats_rejects_nan() {
- <f64 as ReqValue>::compare(&f64::NAN, &0.0);
- }
-
- #[test]
- fn compare_for_integers_matches_ord() {
- assert_eq!(<i64 as ReqValue>::compare(&3, &5), Ordering::Less);
- assert_eq!(<i64 as ReqValue>::compare(&5, &5), Ordering::Equal);
- assert_eq!(<i64 as ReqValue>::compare(&7, &5), Ordering::Greater);
+ .map_err(insufficient_data("failed to read f64 from REQ sketch"))?;
+ Self::new(value).map_err(|_| Error::deserial("REQ float must not be
NaN"))
}
}
diff --git a/tests-integration/tests/req_test/accuracy.rs
b/tests-integration/tests/req_test/accuracy.rs
index 8ca9fde..d68f8c2 100644
--- a/tests-integration/tests/req_test/accuracy.rs
+++ b/tests-integration/tests/req_test/accuracy.rs
@@ -23,13 +23,16 @@ use datasketches::req::SearchCriteria;
use googletest::assert_that;
use googletest::prelude::le;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn rank_space_error_is_bounded() -> Result<(), Error> {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
let n = 50_000;
for i in 0..n {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
assert_eq!(sketch.n(), n as u64);
diff --git a/tests-integration/tests/req_test/bounds.rs
b/tests-integration/tests/req_test/bounds.rs
index 13899af..b89b743 100644
--- a/tests-integration/tests/req_test/bounds.rs
+++ b/tests-integration/tests/req_test/bounds.rs
@@ -27,12 +27,15 @@ use googletest::prelude::ge;
use googletest::prelude::le;
use googletest::prelude::lt;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn bounds_are_nested_and_in_unit_interval() {
- let mut sketch = ReqSketch::new(12, RankAccuracy::HighRank).unwrap();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::new(12,
RankAccuracy::HighRank).unwrap();
for i in 0..50_000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
for rank in [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999] {
@@ -60,18 +63,18 @@ fn bounds_are_nested_and_in_unit_interval() {
#[test]
fn theoretical_error_bounds_cover_uniform_quantiles() -> Result<(), Error> {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
let n = 50_000;
for i in 0..n {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
for rank in [
0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8,
0.85, 0.9, 0.92,
0.95, 0.97, 0.98, 0.99, 0.995, 0.999,
] {
- let true_quantile = rank * (n - 1) as f64;
+ let true_quantile = req_f64(rank * (n - 1) as f64);
let estimated_rank = sketch.rank(&true_quantile,
SearchCriteria::Inclusive)?;
let lower = sketch.rank_lower_bound(rank, 3);
let upper = sketch.rank_upper_bound(rank, 3);
@@ -84,12 +87,12 @@ fn theoretical_error_bounds_cover_uniform_quantiles() ->
Result<(), Error> {
#[test]
fn hra_and_lra_bounds_are_tighter_at_their_target_end() -> Result<(), Error> {
for rank in [0.05, 0.25, 0.5, 0.75, 0.95] {
- let mut hra = ReqSketch::new(12, RankAccuracy::HighRank).unwrap();
- let mut lra = ReqSketch::new(12, RankAccuracy::LowRank).unwrap();
+ let mut hra: ReqSketch<ReqF64> = ReqSketch::new(12,
RankAccuracy::HighRank).unwrap();
+ let mut lra: ReqSketch<ReqF64> = ReqSketch::new(12,
RankAccuracy::LowRank).unwrap();
for i in 0..10_000 {
- hra.update(i as f64);
- lra.update(i as f64);
+ hra.update(req_f64(i as f64));
+ lra.update(req_f64(i as f64));
}
let hra_error =
@@ -109,10 +112,10 @@ fn hra_and_lra_bounds_are_tighter_at_their_target_end()
-> Result<(), Error> {
#[test]
fn exact_mode_bounds_are_tight() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..20 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
assert!(!sketch.is_estimation_mode());
@@ -126,17 +129,17 @@ fn exact_mode_bounds_are_tight() {
#[test]
fn high_rank_accuracy_matches_tight_thresholds() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
let n = 50_000;
for i in 0..n {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
assert_eq!(sketch.n(), n as u64);
for rank in [0.5, 0.9, 0.95, 0.99, 0.999] {
- let true_quantile = rank * (n - 1) as f64;
+ let true_quantile = req_f64(rank * (n - 1) as f64);
let estimated_rank = sketch
.rank(&true_quantile, SearchCriteria::Inclusive)
.expect("rank should succeed");
diff --git a/tests-integration/tests/req_test/core.rs
b/tests-integration/tests/req_test/core.rs
index 375d03e..0b43595 100644
--- a/tests-integration/tests/req_test/core.rs
+++ b/tests-integration/tests/req_test/core.rs
@@ -33,9 +33,14 @@ use googletest::prelude::lt;
use googletest::prelude::near;
use googletest::prelude::none;
+use super::ReqF32;
+use super::ReqF64;
+use super::req_f32;
+use super::req_f64;
+
#[test]
fn empty_sketch_has_default_state_and_rejects_queries() {
- let sketch: ReqSketch<f32> = ReqSketch::default();
+ let sketch: ReqSketch<ReqF64> = ReqSketch::default();
assert_eq!(sketch.k(), 12);
assert!(sketch.is_empty());
@@ -46,7 +51,7 @@ fn empty_sketch_has_default_state_and_rejects_queries() {
assert_that!(sketch.max_item(), none());
assert_that!(
- sketch.rank(&0.0, SearchCriteria::Inclusive),
+ sketch.rank(&req_f64(0.0), SearchCriteria::Inclusive),
err(anything())
);
assert_that!(
@@ -54,66 +59,66 @@ fn empty_sketch_has_default_state_and_rejects_queries() {
err(anything())
);
assert_that!(
- sketch.pmf(&[0.0], SearchCriteria::Inclusive),
+ sketch.pmf(&[req_f64(0.0)], SearchCriteria::Inclusive),
err(anything())
);
assert_that!(
- sketch.cdf(&[0.0], SearchCriteria::Inclusive),
+ sketch.cdf(&[req_f64(0.0)], SearchCriteria::Inclusive),
err(anything())
);
}
#[test]
fn single_value_hra_answers_exactly() {
- let mut sketch = ReqSketch::default();
- sketch.update(1.0f32);
+ let mut sketch: ReqSketch<ReqF32> = ReqSketch::default();
+ sketch.update(req_f32(1.0));
assert!(!sketch.is_empty());
assert!(!sketch.is_estimation_mode());
assert_eq!(sketch.n(), 1);
assert_eq!(sketch.num_retained(), 1);
- assert_eq!(sketch.min_item(), Some(&1.0));
- assert_eq!(sketch.max_item(), Some(&1.0));
+ assert_eq!(sketch.min_item().copied(), Some(req_f32(1.0)));
+ assert_eq!(sketch.max_item().copied(), Some(req_f32(1.0)));
assert_that!(
sketch
- .rank(&1.0, SearchCriteria::Exclusive)
+ .rank(&req_f32(1.0), SearchCriteria::Exclusive)
.expect("rank should succeed"),
approx_eq(0.0)
);
assert_that!(
sketch
- .rank(&1.0, SearchCriteria::Inclusive)
+ .rank(&req_f32(1.0), SearchCriteria::Inclusive)
.expect("rank should succeed"),
approx_eq(1.0)
);
assert_that!(
sketch
- .rank(&1.1, SearchCriteria::Exclusive)
+ .rank(&req_f32(2.0), SearchCriteria::Exclusive)
.expect("rank should succeed"),
approx_eq(1.0)
);
assert_that!(
sketch
- .rank(&f32::INFINITY, SearchCriteria::Inclusive)
+ .rank(&req_f32(f32::MAX), SearchCriteria::Inclusive)
.expect("rank should succeed"),
approx_eq(1.0)
);
for rank in [0.0, 0.5, 1.0] {
- assert_that!(
+ assert_eq!(
sketch
.quantile(rank, SearchCriteria::Exclusive)
.expect("quantile should succeed"),
- approx_eq(1.0)
+ req_f32(1.0)
);
}
}
#[test]
fn single_value_lra_preserves_configuration() {
- let mut sketch = ReqSketch::<f32>::new(12, RankAccuracy::LowRank).unwrap();
- sketch.update(1.0f32);
+ let mut sketch = ReqSketch::<ReqF64>::new(12,
RankAccuracy::LowRank).unwrap();
+ sketch.update(req_f64(1.0));
assert_eq!(sketch.rank_accuracy(), RankAccuracy::LowRank);
assert!(!sketch.is_empty());
@@ -124,12 +129,12 @@ fn single_value_lra_preserves_configuration() {
#[test]
fn repeated_values_respect_search_criteria() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for _ in 0..3 {
- sketch.update(1.0f32);
+ sketch.update(req_f64(1.0));
}
for _ in 0..3 {
- sketch.update(2.0f32);
+ sketch.update(req_f64(2.0));
}
assert!(!sketch.is_estimation_mode());
@@ -138,25 +143,25 @@ fn repeated_values_respect_search_criteria() {
assert_that!(
sketch
- .rank(&1.0, SearchCriteria::Exclusive)
+ .rank(&req_f64(1.0), SearchCriteria::Exclusive)
.expect("rank should succeed"),
approx_eq(0.0)
);
assert_that!(
sketch
- .rank(&1.0, SearchCriteria::Inclusive)
+ .rank(&req_f64(1.0), SearchCriteria::Inclusive)
.expect("rank should succeed"),
approx_eq(0.5)
);
assert_that!(
sketch
- .rank(&2.0, SearchCriteria::Exclusive)
+ .rank(&req_f64(2.0), SearchCriteria::Exclusive)
.expect("rank should succeed"),
approx_eq(0.5)
);
assert_that!(
sketch
- .rank(&2.0, SearchCriteria::Inclusive)
+ .rank(&req_f64(2.0), SearchCriteria::Inclusive)
.expect("rank should succeed"),
approx_eq(1.0)
);
@@ -164,28 +169,28 @@ fn repeated_values_respect_search_criteria() {
#[test]
fn estimation_mode_compresses_and_keeps_min_max() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
let n = 100_000;
for i in 0..n {
- sketch.update(i as f32);
+ sketch.update(req_f64(i as f64));
}
assert!(!sketch.is_empty());
assert!(sketch.is_estimation_mode());
assert_eq!(sketch.n(), n);
assert_that!(sketch.num_retained(), lt(n as u32));
- assert_eq!(sketch.min_item(), Some(&0.0));
- assert_eq!(sketch.max_item(), Some(&((n - 1) as f32)));
+ assert_eq!(sketch.min_item().copied(), Some(req_f64(0.0)));
+ assert_eq!(sketch.max_item().copied(), Some(req_f64((n - 1) as f64)));
let r0 = sketch
- .rank(&0.0, SearchCriteria::Exclusive)
+ .rank(&req_f64(0.0), SearchCriteria::Exclusive)
.expect("rank should succeed");
let rmid = sketch
- .rank(&(n as f32 / 2.0), SearchCriteria::Exclusive)
+ .rank(&req_f64((n / 2) as f64), SearchCriteria::Exclusive)
.expect("rank should succeed");
let rmax = sketch
- .rank(&(n as f32), SearchCriteria::Exclusive)
+ .rank(&req_f64(n as f64), SearchCriteria::Exclusive)
.expect("rank should succeed");
assert_that!(r0, near(0.0, 1e-3));
@@ -194,56 +199,43 @@ fn estimation_mode_compresses_and_keeps_min_max() {
}
#[test]
-fn nan_updates_are_silently_skipped_for_f64() {
- let mut sketch: ReqSketch<f64> = ReqSketch::default();
- sketch.update(f64::NAN);
- sketch.update(f64::NAN);
- assert!(sketch.is_empty());
- assert_eq!(sketch.n(), 0);
-
- sketch.update(1.0);
- sketch.update(f64::NAN);
- sketch.update(2.0);
- assert_eq!(sketch.n(), 2);
- assert_eq!(sketch.min_item(), Some(&1.0));
- assert_eq!(sketch.max_item(), Some(&2.0));
-}
+fn req_float_adapts_the_non_nan_numeric_order() {
+ assert!(ReqF32::new(f32::NAN).is_err());
+ assert!(ReqF64::new(f64::NAN).is_err());
-#[test]
-fn nan_updates_are_silently_skipped_for_f32() {
- let mut sketch: ReqSketch<f32> = ReqSketch::default();
- sketch.update(f32::NAN);
- assert!(sketch.is_empty());
- assert_eq!(sketch.n(), 0);
+ let negative_zero = req_f64(-0.0);
+ let positive_zero = req_f64(0.0);
+ assert_eq!(negative_zero, positive_zero);
+ assert_eq!(negative_zero.cmp(&positive_zero), std::cmp::Ordering::Equal);
- sketch.update(5.0f32);
- sketch.update(f32::NAN);
- assert_eq!(sketch.n(), 1);
- assert_eq!(
- sketch
- .quantile(0.5, SearchCriteria::Inclusive)
- .expect("quantile should succeed"),
- 5.0f32
- );
+ let negative_infinity = req_f64(f64::NEG_INFINITY);
+ let infinity = req_f64(f64::INFINITY);
+ assert!(negative_infinity < infinity);
}
#[test]
fn small_edge_cases_answer_reasonably() -> Result<(), Error> {
- let mut single = ReqSketch::default();
- single.update(42.0);
- assert_eq!(single.quantile(0.5, SearchCriteria::Inclusive)?, 42.0);
+ let mut single: ReqSketch<ReqF64> = ReqSketch::default();
+ single.update(req_f64(42.0));
+ assert_eq!(
+ single.quantile(0.5, SearchCriteria::Inclusive)?,
+ req_f64(42.0)
+ );
let mut two_values = ReqSketch::default();
- two_values.update(1.0);
- two_values.update(100.0);
+ two_values.update(req_f64(1.0));
+ two_values.update(req_f64(100.0));
let median = two_values.quantile(0.5, SearchCriteria::Inclusive)?;
- assert_that!(median, all!(ge(1.0), le(100.0)));
+ assert_that!(*median, all!(ge(1.0), le(100.0)));
let mut duplicates = ReqSketch::default();
for _ in 0..100 {
- duplicates.update(42.0);
+ duplicates.update(req_f64(42.0));
}
- assert_eq!(duplicates.quantile(0.5, SearchCriteria::Inclusive)?, 42.0);
+ assert_eq!(
+ duplicates.quantile(0.5, SearchCriteria::Inclusive)?,
+ req_f64(42.0)
+ );
Ok(())
}
@@ -251,14 +243,14 @@ fn small_edge_cases_answer_reasonably() -> Result<(),
Error> {
#[test]
fn new_validates_k() {
assert_that!(
- ReqSketch::<f64>::new(0, RankAccuracy::HighRank),
+ ReqSketch::<ReqF64>::new(0, RankAccuracy::HighRank),
err(anything())
);
- let error = ReqSketch::<f64>::new(3, RankAccuracy::HighRank).unwrap_err();
+ let error = ReqSketch::<ReqF64>::new(3,
RankAccuracy::HighRank).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidArgument);
assert_that!(
- ReqSketch::<f64>::new(4096, RankAccuracy::HighRank),
+ ReqSketch::<ReqF64>::new(4096, RankAccuracy::HighRank),
err(anything())
);
- assert!(ReqSketch::<f64>::new(12, RankAccuracy::HighRank).is_ok());
+ assert!(ReqSketch::<ReqF64>::new(12, RankAccuracy::HighRank).is_ok());
}
diff --git a/tests-integration/tests/req_test/main.rs
b/tests-integration/tests/req_test/generic.rs
similarity index 58%
copy from tests-integration/tests/req_test/main.rs
copy to tests-integration/tests/req_test/generic.rs
index 4633a38..26da858 100644
--- a/tests-integration/tests/req_test/main.rs
+++ b/tests-integration/tests/req_test/generic.rs
@@ -15,11 +15,23 @@
// specific language governing permissions and limitations
// under the License.
-mod accuracy;
-mod bounds;
-mod core;
-mod merge;
-mod property;
-mod query;
-mod sorted_view_api;
-mod structure;
+use datasketches::req::ReqSketch;
+use datasketches::req::SearchCriteria;
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+struct Reading(i32);
+
+#[test]
+fn custom_items_do_not_need_serialization() {
+ let mut sketch = ReqSketch::default();
+ sketch.update(Reading(30));
+ sketch.update(Reading(10));
+ sketch.update(Reading(20));
+
+ 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(),
+ Reading(20)
+ );
+}
diff --git a/tests-integration/tests/req_test/main.rs
b/tests-integration/tests/req_test/main.rs
index 4633a38..a76589b 100644
--- a/tests-integration/tests/req_test/main.rs
+++ b/tests-integration/tests/req_test/main.rs
@@ -15,9 +15,23 @@
// specific language governing permissions and limitations
// under the License.
+use datasketches::req::ReqFloat;
+
+type ReqF32 = ReqFloat<f32>;
+type ReqF64 = ReqFloat<f64>;
+
+fn req_f32(value: f32) -> ReqF32 {
+ ReqF32::new(value).unwrap()
+}
+
+fn req_f64(value: f64) -> ReqF64 {
+ ReqF64::new(value).unwrap()
+}
+
mod accuracy;
mod bounds;
mod core;
+mod generic;
mod merge;
mod property;
mod query;
diff --git a/tests-integration/tests/req_test/merge.rs
b/tests-integration/tests/req_test/merge.rs
index 78b6c74..6f1b9ca 100644
--- a/tests-integration/tests/req_test/merge.rs
+++ b/tests-integration/tests/req_test/merge.rs
@@ -25,18 +25,21 @@ use googletest::prelude::anything;
use googletest::prelude::err;
use googletest::prelude::near;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn merge_into_empty_preserves_source_distribution() {
- let mut target: ReqSketch<f32> = ReqSketch::new(40,
RankAccuracy::HighRank).unwrap();
- let mut source: ReqSketch<f32> = ReqSketch::new(40,
RankAccuracy::HighRank).unwrap();
+ let mut target: ReqSketch<ReqF64> = ReqSketch::new(40,
RankAccuracy::HighRank).unwrap();
+ let mut source: ReqSketch<ReqF64> = ReqSketch::new(40,
RankAccuracy::HighRank).unwrap();
for i in 0..1000 {
- source.update(i as f32);
+ source.update(req_f64(i as f64));
}
target.merge(&source).expect("merge should succeed");
- assert_eq!(target.min_item(), Some(&0.0));
- assert_eq!(target.max_item(), Some(&999.0));
+ assert_eq!(target.min_item().copied(), Some(req_f64(0.0)));
+ assert_eq!(target.max_item().copied(), Some(req_f64(999.0)));
let q25 = target
.quantile(0.25, SearchCriteria::Inclusive)
@@ -48,30 +51,30 @@ fn merge_into_empty_preserves_source_distribution() {
.quantile(0.75, SearchCriteria::Inclusive)
.expect("quantile should succeed");
let r50 = target
- .rank(&500.0, SearchCriteria::Inclusive)
+ .rank(&req_f64(500.0), SearchCriteria::Inclusive)
.expect("rank should succeed");
- assert_that!(q25, near(250.0, 250.0 * 0.01));
- assert_that!(q50, near(500.0, 500.0 * 0.01));
- assert_that!(q75, near(750.0, 750.0 * 0.01));
+ assert_that!(*q25, near(250.0, 250.0 * 0.01));
+ assert_that!(*q50, near(500.0, 500.0 * 0.01));
+ assert_that!(*q75, near(750.0, 750.0 * 0.01));
assert_that!(r50, near(0.5, 0.5 * 0.01));
}
#[test]
fn merge_two_ranges_preserves_distribution() {
- let mut left: ReqSketch<f32> = ReqSketch::new(100,
RankAccuracy::HighRank).unwrap();
- let mut right: ReqSketch<f32> = ReqSketch::new(100,
RankAccuracy::HighRank).unwrap();
+ let mut left: ReqSketch<ReqF64> = ReqSketch::new(100,
RankAccuracy::HighRank).unwrap();
+ let mut right: ReqSketch<ReqF64> = ReqSketch::new(100,
RankAccuracy::HighRank).unwrap();
for i in 0..1000 {
- left.update(i as f32);
+ left.update(req_f64(i as f64));
}
for i in 1000..2000 {
- right.update(i as f32);
+ right.update(req_f64(i as f64));
}
left.merge(&right).expect("merge should succeed");
- assert_eq!(left.min_item(), Some(&0.0));
- assert_eq!(left.max_item(), Some(&1999.0));
+ assert_eq!(left.min_item().copied(), Some(req_f64(0.0)));
+ assert_eq!(left.max_item().copied(), Some(req_f64(1999.0)));
let q25 = left
.quantile(0.25, SearchCriteria::Inclusive)
@@ -83,42 +86,42 @@ fn merge_two_ranges_preserves_distribution() {
.quantile(0.75, SearchCriteria::Inclusive)
.expect("quantile should succeed");
let r50 = left
- .rank(&1000.0, SearchCriteria::Inclusive)
+ .rank(&req_f64(1000.0), SearchCriteria::Inclusive)
.expect("rank should succeed");
- assert_that!(q25, near(500.0, 500.0 * 0.02));
- assert_that!(q50, near(1000.0, 1000.0 * 0.01));
- assert_that!(q75, near(1500.0, 1500.0 * 0.01));
+ assert_that!(*q25, near(500.0, 500.0 * 0.02));
+ assert_that!(*q50, near(1000.0, 1000.0 * 0.01));
+ assert_that!(*q75, near(1500.0, 1500.0 * 0.01));
assert_that!(r50, near(0.5, 0.5 * 0.01));
}
#[test]
fn merge_rejects_incompatible_accuracy_modes() {
- let mut high_rank = ReqSketch::default();
- let low_rank: ReqSketch<f32> = ReqSketch::new(12,
RankAccuracy::LowRank).unwrap();
+ let mut high_rank: ReqSketch<ReqF64> = ReqSketch::default();
+ let low_rank: ReqSketch<ReqF64> = ReqSketch::new(12,
RankAccuracy::LowRank).unwrap();
- high_rank.update(1.0);
+ high_rank.update(req_f64(1.0));
assert_that!(high_rank.merge(&low_rank), err(anything()));
}
#[test]
fn many_small_merges_preserve_count_bounds_and_median() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for batch in 0..100 {
let mut batch_sketch = ReqSketch::default();
for i in 0..100 {
- batch_sketch.update((batch * 100 + i) as f64);
+ batch_sketch.update(req_f64((batch * 100 + i) as f64));
}
sketch.merge(&batch_sketch).expect("merge should succeed");
}
assert_eq!(sketch.n(), 10_000);
- assert_eq!(sketch.min_item(), Some(&0.0));
- assert_eq!(sketch.max_item(), Some(&9999.0));
+ assert_eq!(sketch.min_item().copied(), Some(req_f64(0.0)));
+ assert_eq!(sketch.max_item().copied(), Some(req_f64(9999.0)));
let median = sketch
.quantile(0.5, SearchCriteria::Inclusive)
.expect("quantile should succeed");
- assert_that!(median, near(4999.5, 500.0));
+ 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 b70952d..e993c4d 100644
--- a/tests-integration/tests/req_test/property.rs
+++ b/tests-integration/tests/req_test/property.rs
@@ -23,16 +23,19 @@ use quickcheck::Gen;
use quickcheck::QuickCheck;
use quickcheck::TestResult;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn prop_quantile_rank_consistency() {
- fn property(values: Vec<u64>) -> TestResult {
+ fn property(values: Vec<u32>) -> TestResult {
if !(500..1500).contains(&values.len()) {
return TestResult::discard();
}
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for value in values {
- sketch.update(value);
+ sketch.update(req_f64(value as f64));
}
// These sizes push the sketch past the compaction threshold, so the
@@ -68,23 +71,23 @@ fn prop_quantile_rank_consistency() {
.tests(256)
.min_tests_passed(256)
.rng(Gen::new(1500))
- .quickcheck(property as fn(Vec<u64>) -> TestResult);
+ .quickcheck(property as fn(Vec<u32>) -> TestResult);
}
#[test]
fn prop_sketch_bounds() {
- fn property(values: Vec<i64>) -> TestResult {
+ fn property(values: Vec<i32>) -> TestResult {
if !(1..1000).contains(&values.len()) {
return TestResult::discard();
}
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for value in &values {
- sketch.update(*value);
+ sketch.update(req_f64(*value as f64));
}
- let true_min = values.iter().copied().min().expect("values are
non-empty");
- let true_max = values.iter().copied().max().expect("values are
non-empty");
+ let true_min = req_f64(values.iter().copied().min().expect("values are
non-empty") as f64);
+ let true_max = req_f64(values.iter().copied().max().expect("values are
non-empty") as f64);
assert_eq!(sketch.min_item(), Some(&true_min));
assert_eq!(sketch.max_item(), Some(&true_max));
@@ -109,7 +112,7 @@ fn prop_sketch_bounds() {
.tests(256)
.min_tests_passed(256)
.rng(Gen::new(1000))
- .quickcheck(property as fn(Vec<i64>) -> TestResult);
+ .quickcheck(property as fn(Vec<i32>) -> TestResult);
}
#[test]
@@ -119,9 +122,9 @@ fn prop_rank_monotonicity() {
return TestResult::discard();
}
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for value in values {
- sketch.update(value);
+ sketch.update(req_f64(value as f64));
}
let mut last_rank = -1.0;
@@ -134,7 +137,7 @@ fn prop_rank_monotonicity() {
u32::MAX,
] {
let rank = sketch
- .rank(&value, SearchCriteria::Inclusive)
+ .rank(&req_f64(value as f64), SearchCriteria::Inclusive)
.expect("rank should succeed");
assert!(rank >= last_rank, "rank {} after {}", rank, last_rank);
assert!((0.0..=1.0).contains(&rank), "rank {} out of bounds",
rank);
diff --git a/tests-integration/tests/req_test/query.rs
b/tests-integration/tests/req_test/query.rs
index 30f3092..0404dec 100644
--- a/tests-integration/tests/req_test/query.rs
+++ b/tests-integration/tests/req_test/query.rs
@@ -27,11 +27,14 @@ use googletest::prelude::le;
use googletest::prelude::lt;
use googletest::prelude::near;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 1..=10 {
- sketch.update(i as f32);
+ sketch.update(req_f64(i as f64));
}
assert!(!sketch.is_estimation_mode());
@@ -41,7 +44,7 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
for (value, expected) in [(1.0, 0.0), (2.0, 0.1), (6.0, 0.5), (9.0, 0.8),
(10.0, 0.9)] {
assert_that!(
sketch
- .rank(&value, SearchCriteria::Exclusive)
+ .rank(&req_f64(value), SearchCriteria::Exclusive)
.expect("rank should succeed"),
near(expected, 1e-6)
);
@@ -50,31 +53,31 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
for (value, expected) in [(1.0, 0.1), (2.0, 0.2), (5.0, 0.5), (9.0, 0.9),
(10.0, 1.0)] {
assert_that!(
sketch
- .rank(&value, SearchCriteria::Inclusive)
+ .rank(&req_f64(value), SearchCriteria::Inclusive)
.expect("rank should succeed"),
near(expected, 1e-6)
);
}
for (rank, expected) in [(0.0, 1.0), (0.1, 2.0), (0.5, 6.0), (0.9, 10.0),
(1.0, 10.0)] {
- assert_that!(
- sketch
+ assert_eq!(
+ *sketch
.quantile(rank, SearchCriteria::Exclusive)
.expect("quantile should succeed"),
- near(expected, 1e-6)
+ expected
);
}
for (rank, expected) in [(0.0, 1.0), (0.1, 1.0), (0.5, 5.0), (0.9, 9.0),
(1.0, 10.0)] {
- assert_that!(
- sketch
+ assert_eq!(
+ *sketch
.quantile(rank, SearchCriteria::Inclusive)
.expect("quantile should succeed"),
- near(expected, 1e-6)
+ expected
);
}
- let splits = [2.0, 6.0, 9.0];
+ let splits = [2.0, 6.0, 9.0].map(req_f64);
let cdf = sketch
.cdf(&splits, SearchCriteria::Exclusive)
.expect("cdf should succeed");
@@ -94,12 +97,12 @@ fn exact_mode_rank_quantile_pmf_and_cdf_match_reference() {
#[test]
fn pmf_and_cdf_are_consistent() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..1000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
- let split_points = [100.0, 300.0, 500.0, 700.0, 900.0];
+ 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");
@@ -119,12 +122,12 @@ fn pmf_and_cdf_are_consistent() {
#[test]
fn rank_is_monotonic_and_bounded() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..10_000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
- let test_values: Vec<f64> = (0..10_000).step_by(1000).map(|i| i as
f64).collect();
+ let test_values = (0..10_000).step_by(1000).map(|value| req_f64(value as
f64));
let mut last_rank = 0.0;
for value in test_values {
@@ -139,9 +142,9 @@ fn rank_is_monotonic_and_bounded() {
#[test]
fn quantiles_are_monotonic() -> Result<(), Error> {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..10_000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
let ranks = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
@@ -149,8 +152,8 @@ fn quantiles_are_monotonic() -> Result<(), Error> {
for rank in ranks {
let quantile = sketch.quantile(rank, SearchCriteria::Inclusive)?;
- assert_that!(quantile, ge(previous));
- previous = quantile;
+ assert_that!(*quantile, ge(previous));
+ previous = *quantile;
}
Ok(())
@@ -158,9 +161,9 @@ fn quantiles_are_monotonic() -> Result<(), Error> {
#[test]
fn rank_quantile_round_trip_is_consistent() -> Result<(), Error> {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..10_000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
for target_rank in [0.1, 0.25, 0.5, 0.75, 0.9] {
@@ -175,12 +178,12 @@ fn rank_quantile_round_trip_is_consistent() -> Result<(),
Error> {
#[test]
fn search_criteria_rank_consistency() -> Result<(), Error> {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..1000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
- for value in [100.0, 250.0, 500.0, 750.0] {
+ 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)?;
@@ -194,16 +197,22 @@ fn search_criteria_rank_consistency() -> Result<(),
Error> {
#[test]
fn signed_zeros_share_rank_and_cannot_be_distinct_splits() -> Result<(),
Error> {
- let mut sketch = ReqSketch::default();
- sketch.update(-0.0_f64);
- sketch.update(0.0_f64);
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
+ let negative_zero = req_f64(-0.0);
+ let positive_zero = req_f64(0.0);
+ sketch.update(negative_zero);
+ sketch.update(positive_zero);
- for value in [-0.0, 0.0] {
+ 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!(sketch.pmf(&[-0.0, 0.0], SearchCriteria::Inclusive).is_err());
+ assert!(
+ sketch
+ .pmf(&[negative_zero, positive_zero], SearchCriteria::Inclusive)
+ .is_err()
+ );
Ok(())
}
diff --git a/tests-integration/tests/req_test/sorted_view_api.rs
b/tests-integration/tests/req_test/sorted_view_api.rs
index 32a518b..6882f2a 100644
--- a/tests-integration/tests/req_test/sorted_view_api.rs
+++ b/tests-integration/tests/req_test/sorted_view_api.rs
@@ -32,28 +32,33 @@ use googletest::prelude::ge;
use googletest::prelude::lt;
use googletest::prelude::near;
-fn populated_sketch(n: u64) -> ReqSketch<f64> {
+use super::ReqF64;
+use super::req_f64;
+
+fn populated_sketch(n: i64) -> ReqSketch<ReqF64> {
let mut sketch = ReqSketch::default();
for i in 0..n {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
sketch
}
/// All distribution queries must work through a shared (`&self`) reference.
-fn query_through_shared_ref(sketch: &ReqSketch<f64>) {
+fn query_through_shared_ref(sketch: &ReqSketch<ReqF64>) {
sketch
.quantile(0.5, SearchCriteria::Inclusive)
.expect("quantile");
sketch
.quantiles(&[0.25, 0.5, 0.75], SearchCriteria::Inclusive)
.expect("quantiles");
- sketch.rank(&50.0, SearchCriteria::Inclusive).expect("rank");
sketch
- .pmf(&[10.0, 50.0], SearchCriteria::Inclusive)
+ .rank(&req_f64(50.0), SearchCriteria::Inclusive)
+ .expect("rank");
+ sketch
+ .pmf(&[req_f64(10.0), req_f64(50.0)], SearchCriteria::Inclusive)
.expect("pmf");
sketch
- .cdf(&[10.0, 50.0], SearchCriteria::Inclusive)
+ .cdf(&[req_f64(10.0), req_f64(50.0)], SearchCriteria::Inclusive)
.expect("cdf");
assert!(!sketch.sorted_view().is_empty());
}
@@ -68,13 +73,13 @@ fn queries_work_through_shared_reference() {
fn sorted_view_is_an_owned_snapshot() {
let mut sketch = populated_sketch(100);
- let view: SortedView<f64> = sketch.sorted_view();
+ let view: SortedView<ReqF64> = sketch.sorted_view();
assert_eq!(view.total_weight(), 100);
// Updating the sketch while the view is alive must compile (owned view)
// and must not affect the snapshot.
for i in 100..200 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
assert_eq!(view.total_weight(), 100);
@@ -85,7 +90,7 @@ fn sorted_view_is_an_owned_snapshot() {
#[test]
fn sorted_view_on_empty_sketch_is_an_empty_view() {
- let sketch: ReqSketch<f64> = ReqSketch::default();
+ let sketch: ReqSketch<ReqF64> = ReqSketch::default();
let view = sketch.sorted_view();
assert!(view.is_empty());
assert_eq!(view.len(), 0);
@@ -99,13 +104,13 @@ fn sorted_view_on_empty_sketch_is_an_empty_view() {
#[test]
fn empty_sketch_pmf_cdf_report_error() {
- let sketch: ReqSketch<f64> = ReqSketch::default();
+ let sketch: ReqSketch<ReqF64> = ReqSketch::default();
assert_that!(
- sketch.pmf(&[1.0], SearchCriteria::Inclusive),
+ sketch.pmf(&[req_f64(1.0)], SearchCriteria::Inclusive),
err(anything())
);
assert_that!(
- sketch.cdf(&[1.0], SearchCriteria::Inclusive),
+ sketch.cdf(&[req_f64(1.0)], SearchCriteria::Inclusive),
err(anything())
);
}
@@ -114,49 +119,16 @@ fn empty_sketch_pmf_cdf_report_error() {
fn view_rank_is_primary_query_name() {
let sketch = populated_sketch(10);
let view = sketch.sorted_view();
- let r = view.rank(&5.0, SearchCriteria::Inclusive).expect("rank");
+ let r = view
+ .rank(&req_f64(5.0), SearchCriteria::Inclusive)
+ .expect("rank");
assert_that!(r, near(0.6, 1e-10));
}
-#[test]
-fn nan_query_items_and_split_points_are_rejected() {
- let sketch = populated_sketch(100);
- let error = sketch
- .rank(&f64::NAN, SearchCriteria::Inclusive)
- .unwrap_err();
- assert_eq!(error.kind(), ErrorKind::InvalidArgument);
- assert_eq!(
- sketch
- .pmf(&[f64::NAN], SearchCriteria::Inclusive)
- .unwrap_err()
- .kind(),
- ErrorKind::InvalidArgument
- );
- assert_eq!(
- sketch
- .cdf(&[50.0, f64::NAN], SearchCriteria::Inclusive)
- .unwrap_err()
- .kind(),
- ErrorKind::InvalidArgument
- );
-
- let view = sketch.sorted_view();
- assert_that!(
- view.rank(&f64::NAN, SearchCriteria::Inclusive),
- err(anything())
- );
- assert_eq!(
- view.pmf(&[f64::NAN], SearchCriteria::Inclusive)
- .unwrap_err()
- .kind(),
- ErrorKind::InvalidArgument
- );
-}
-
#[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<f64> = ReqSketch::default();
+ 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"));
@@ -170,8 +142,8 @@ fn error_precedence_empty_before_invalid_rank() {
#[test]
fn view_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
- assert_send_sync::<SortedView<f64>>();
- assert_send_sync::<ReqSketch<f64>>();
+ assert_send_sync::<SortedView<ReqF64>>();
+ assert_send_sync::<ReqSketch<ReqF64>>();
}
#[test]
@@ -190,6 +162,6 @@ fn concurrent_readers_share_the_sketch() {
.collect();
for handle in handles {
let q = handle.join().expect("thread");
- assert_that!(q, all!(ge(0.0), lt(1_000.0)));
+ assert_that!(*q, all!(ge(0.0), lt(1_000.0)));
}
}
diff --git a/tests-integration/tests/req_test/structure.rs
b/tests-integration/tests/req_test/structure.rs
index cbe2838..e14cd79 100644
--- a/tests-integration/tests/req_test/structure.rs
+++ b/tests-integration/tests/req_test/structure.rs
@@ -26,11 +26,14 @@ use googletest::prelude::gt;
use googletest::prelude::le;
use googletest::prelude::predicate;
+use super::ReqF64;
+use super::req_f64;
+
#[test]
fn iterator_weights_sum_to_n_and_items_are_in_range() {
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..1000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
let total_weight: u64 = sketch.iter().map(|(_, weight)| weight).sum();
@@ -46,12 +49,12 @@ fn iterator_weights_sum_to_n_and_items_are_in_range() {
#[test]
fn small_sketch_iterator_reports_unit_weights() {
// Below the compaction threshold every retained item still has weight 1.
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..10 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
- let items: Vec<(f64, u64)> = sketch.iter().collect();
+ let items: Vec<(ReqF64, u64)> = sketch.iter().collect();
assert_eq!(items.len(), 10);
let weights: Vec<_> = items.iter().map(|&(_, weight)| weight).collect();
assert_that!(weights, each(eq(&1)));
@@ -59,7 +62,7 @@ fn small_sketch_iterator_reports_unit_weights() {
#[test]
fn empty_sketch_iterator_yields_nothing() {
- let sketch: ReqSketch<i32> = ReqSketch::default();
+ let sketch: ReqSketch<ReqF64> = ReqSketch::default();
assert_eq!(sketch.iter().count(), 0);
}
@@ -67,9 +70,9 @@ fn empty_sketch_iterator_yields_nothing() {
fn compaction_promotes_surviving_items_to_higher_weights() {
// After enough updates to trigger compaction, surviving items are
promoted up a
// level at double weight, so the maximum item weight exceeds 1.
- let mut sketch = ReqSketch::default();
+ let mut sketch: ReqSketch<ReqF64> = ReqSketch::default();
for i in 0..100_000 {
- sketch.update(i as f64);
+ sketch.update(req_f64(i as f64));
}
let max_weight = sketch.iter().map(|(_, weight)| weight).max().unwrap();
diff --git a/tests-integration/tests/serde_tests/req.rs
b/tests-integration/tests/serde_tests/req.rs
index 20e61d6..0138664 100644
--- a/tests-integration/tests/serde_tests/req.rs
+++ b/tests-integration/tests/serde_tests/req.rs
@@ -21,6 +21,7 @@ use std::fs;
use std::path::PathBuf;
use datasketches::req::RankAccuracy;
+use datasketches::req::ReqFloat;
use datasketches::req::ReqSketch;
use datasketches::req::ReqValue;
use datasketches::req::SearchCriteria;
@@ -31,11 +32,22 @@ use googletest::prelude::ok;
use crate::serialization_test_data;
+type ReqF32 = ReqFloat<f32>;
+type ReqF64 = ReqFloat<f64>;
+
+fn req_f32(value: f32) -> ReqF32 {
+ ReqFloat::<f32>::new(value).unwrap()
+}
+
+fn req_f64(value: f64) -> ReqF64 {
+ ReqFloat::<f64>::new(value).unwrap()
+}
+
// ---------- Rust ↔ Rust round-trip ----------
fn round_trip_one<T>(k: u16, ra: RankAccuracy, n: u64, make_item: impl Fn(u64)
-> T)
where
- T: ReqValue + std::fmt::Debug + PartialEq,
+ T: Clone + Ord + ReqValue + std::fmt::Debug,
{
let mut a: ReqSketch<T> = ReqSketch::new(k, ra).unwrap();
for i in 0..n {
@@ -56,7 +68,7 @@ fn round_trip_f64_matrix() {
for &k in &[4u16, 12, 1024] {
for &ra in &[RankAccuracy::HighRank, RankAccuracy::LowRank] {
for &n in &[0u64, 1, 4, 5, 100, 10_000] {
- round_trip_one::<f64>(k, ra, n, |i| i as f64);
+ round_trip_one::<ReqF64>(k, ra, n, |i| req_f64(i as f64));
}
}
}
@@ -65,7 +77,7 @@ fn round_trip_f64_matrix() {
#[test]
fn round_trip_f32_basic() {
for &n in &[0u64, 1, 4, 5, 1000] {
- round_trip_one::<f32>(12, RankAccuracy::HighRank, n, |i| i as f32);
+ round_trip_one::<ReqF32>(12, RankAccuracy::HighRank, n, |i| req_f32(i
as f32));
}
}
@@ -88,7 +100,7 @@ fn deserialize_truncated_preamble() {
// Less than 8 bytes — can't even read the fixed preamble.
for n in 0..8usize {
let bytes = vec![0u8; n];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()), "preamble length: {n}");
}
}
@@ -106,7 +118,7 @@ fn deserialize_wrong_family_id() {
0u8, // num_levels
0u8, // num_raw_items
];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
let err = result.unwrap_err();
assert_eq!(
@@ -125,7 +137,7 @@ fn deserialize_wrong_serial_version() {
17u8, 4u8, // IS_EMPTY
12u8, 0u8, 0u8, 0u8,
];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
}
@@ -133,7 +145,7 @@ fn deserialize_wrong_serial_version() {
fn deserialize_invalid_preamble_ints() {
// preamble_ints must be 2 (exact) or 4 (estimation). Try 3.
let bytes = [3u8, 1, 17, 4, 12, 0, 0, 0];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
}
@@ -148,7 +160,7 @@ fn deserialize_rejects_non_empty_zero_levels() {
0u8, // num_levels=0 is invalid for non-empty sketches
0u8,
];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
}
@@ -161,7 +173,7 @@ fn deserialize_rejects_inconsistent_raw_items_header() {
0u8, // invalid raw item count
];
assert_that!(
- ReqSketch::<f32>::deserialize(&raw_with_no_items),
+ ReqSketch::<ReqF32>::deserialize(&raw_with_no_items),
err(anything())
);
@@ -171,7 +183,7 @@ fn deserialize_rejects_inconsistent_raw_items_header() {
1u8,
];
assert_that!(
- ReqSketch::<f32>::deserialize(&raw_with_two_levels),
+ ReqSketch::<ReqF32>::deserialize(&raw_with_two_levels),
err(anything())
);
}
@@ -212,7 +224,7 @@ fn deserialize_truncated_estimation_mode() {
0u8, /* num_raw_items
* no payload — truncated */
];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
}
@@ -226,28 +238,28 @@ fn deserialize_truncated_raw_items() {
3u8, // num_raw_items=3 (but only 1 f32 supplied)
0u8, 0, 0x80, 0x3f, // 1.0_f32 (only 1 of the 3 promised items)
];
- let result = ReqSketch::<f32>::deserialize(&bytes);
+ let result = ReqSketch::<ReqF32>::deserialize(&bytes);
assert_that!(result, err(anything()));
}
#[test]
fn merge_preserves_order_across_serde_round_trip() {
- let mut high = ReqSketch::<f64>::default();
- let mut low = ReqSketch::<f64>::default();
+ let mut high = ReqSketch::<ReqF64>::default();
+ let mut low = ReqSketch::<ReqF64>::default();
for value in 1000..=1072 {
- high.update(value as f64);
+ high.update(req_f64(value as f64));
}
for value in 0..=72 {
- low.update(value as f64);
+ low.update(req_f64(value as f64));
}
high.merge(&low).unwrap();
- let restored = ReqSketch::<f64>::deserialize(&high.serialize()).unwrap();
+ let restored =
ReqSketch::<ReqF64>::deserialize(&high.serialize()).unwrap();
let view = restored.sorted_view();
for value in 0..=1072 {
- let value = value as f64;
+ let value = req_f64(value as f64);
assert_eq!(
restored.rank(&value, SearchCriteria::Inclusive).unwrap(),
view.rank(&value, SearchCriteria::Inclusive).unwrap(),
@@ -281,9 +293,9 @@ fn exact_image(k: u16, items: &[f32]) -> Vec<u8> {
}
fn estimation_image(k: u16, n: u64) -> Vec<u8> {
- let mut sketch = ReqSketch::<f32>::new(k, RankAccuracy::HighRank).unwrap();
+ let mut sketch = ReqSketch::<ReqF32>::new(k,
RankAccuracy::HighRank).unwrap();
for item in 1..=n {
- sketch.update(item as f32);
+ sketch.update(req_f32(item as f32));
}
let bytes = sketch.serialize();
assert!(bytes[6] > 1);
@@ -295,14 +307,14 @@ fn read_u64(bytes: &[u8], offset: usize) -> u64 {
}
fn assert_invalid_data(bytes: &[u8]) {
- let error = ReqSketch::<f32>::deserialize(bytes).unwrap_err();
+ let error = ReqSketch::<ReqF32>::deserialize(bytes).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidData);
}
#[test]
fn canonical_exact_image_is_valid() {
let bytes = exact_image(12, &[1.0, 2.0, 3.0, 4.0, 5.0]);
- assert_that!(ReqSketch::<f32>::deserialize(&bytes), ok(anything()));
+ assert_that!(ReqSketch::<ReqF32>::deserialize(&bytes), ok(anything()));
}
#[test]
@@ -347,12 +359,12 @@ fn deserialize_accepts_java_minimum_section_schedule() {
.copy_from_slice(&java_raw.to_le_bytes());
bytes[compactor + NUM_SECTIONS_OFFSET] = 6;
- let mut sketch = ReqSketch::<f32>::deserialize(&bytes).unwrap();
+ let mut sketch = ReqSketch::<ReqF32>::deserialize(&bytes).unwrap();
for item in 1_251..=2_500 {
- sketch.update(item as f32);
+ sketch.update(req_f32(item as f32));
}
let continued = sketch.serialize();
- assert_that!(ReqSketch::<f32>::deserialize(&continued), ok(anything()));
+ assert_that!(ReqSketch::<ReqF32>::deserialize(&continued), ok(anything()));
}
#[test]
@@ -366,10 +378,10 @@ fn deserialize_accepts_safe_section_size_drift() {
// One ULP below 5.0 rounds to a section size of 4 rather than 6.
bytes[raw_offset..raw_offset + 4].copy_from_slice(&(raw_bits -
1).to_le_bytes());
- let mut sketch = ReqSketch::<f32>::deserialize(&bytes).unwrap();
- sketch.update(2_563.0);
+ let mut sketch = ReqSketch::<ReqF32>::deserialize(&bytes).unwrap();
+ sketch.update(req_f32(2_563.0));
assert_that!(
- ReqSketch::<f32>::deserialize(&sketch.serialize()),
+ ReqSketch::<ReqF32>::deserialize(&sketch.serialize()),
ok(anything())
);
}
@@ -388,10 +400,10 @@ fn deserialize_accepts_opaque_compactor_state() {
bytes[compactor + SECTION_SIZE_RAW_OFFSET..compactor +
SECTION_SIZE_RAW_OFFSET + 4]
.copy_from_slice(&raw.to_le_bytes());
bytes[compactor + NUM_SECTIONS_OFFSET] = 12;
- let mut sketch = ReqSketch::<f32>::deserialize(&bytes).unwrap();
- sketch.update(1_001.0);
+ let mut sketch = ReqSketch::<ReqF32>::deserialize(&bytes).unwrap();
+ sketch.update(req_f32(1_001.0));
assert_that!(
- ReqSketch::<f32>::deserialize(&sketch.serialize()),
+ ReqSketch::<ReqF32>::deserialize(&sketch.serialize()),
ok(anything())
);
}
@@ -414,7 +426,7 @@ fn
complementary_compactor_states_do_not_overflow_on_merge() {
bytes[compactor + SECTION_SIZE_RAW_OFFSET..compactor +
SECTION_SIZE_RAW_OFFSET + 4]
.copy_from_slice(&raw.to_le_bytes());
bytes[compactor + NUM_SECTIONS_OFFSET] = 48;
- ReqSketch::<f32>::deserialize(&bytes).unwrap()
+ ReqSketch::<ReqF32>::deserialize(&bytes).unwrap()
});
let (left, right) = sketches.split_at_mut(1);
left[0].merge(&right[0]).unwrap();
@@ -424,12 +436,14 @@ fn
complementary_compactor_states_do_not_overflow_on_merge() {
fn deserialize_normalizes_false_sorted_claim_and_rejects_nan() {
let mut unsorted = exact_image(12, &[3.0, 4.0, 5.0, 1.0, 2.0]);
unsorted[3] |= FLAG_LEVEL_ZERO_SORTED;
- let sketch = ReqSketch::<f32>::deserialize(&unsorted).unwrap();
+ let sketch = ReqSketch::<ReqF32>::deserialize(&unsorted).unwrap();
assert_eq!(
- sketch.rank(&2.0, SearchCriteria::Inclusive).unwrap(),
+ sketch
+ .rank(&req_f32(2.0), SearchCriteria::Inclusive)
+ .unwrap(),
sketch
.sorted_view()
- .rank(&2.0, SearchCriteria::Inclusive)
+ .rank(&req_f32(2.0), SearchCriteria::Inclusive)
.unwrap(),
);
@@ -475,7 +489,7 @@ fn deserialize_rejects_oversized_compactor_num_items() {
fn validate_cross_language_fixture(path: PathBuf, expected_n: u64) {
let bytes =
fs::read(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}",
path.display()));
- let sketch = ReqSketch::<f32>::deserialize(&bytes)
+ let sketch = ReqSketch::<ReqF32>::deserialize(&bytes)
.unwrap_or_else(|e| panic!("deserialize failed for {}: {e}",
path.display()));
assert_eq!(sketch.n(), expected_n, "n mismatch on {}", path.display());
@@ -483,8 +497,14 @@ fn validate_cross_language_fixture(path: PathBuf,
expected_n: u64) {
assert_eq!(sketch.rank_accuracy(), RankAccuracy::HighRank);
if expected_n > 0 {
- assert_eq!(sketch.min_item().copied(), Some(1.0_f32));
- assert_eq!(sketch.max_item().copied(), Some(expected_n as f32));
+ assert_eq!(
+ sketch.min_item().copied().map(ReqFloat::into_inner),
+ Some(1.0)
+ );
+ assert_eq!(
+ sketch.max_item().copied().map(ReqFloat::into_inner),
+ Some(expected_n as f32)
+ );
let _ = sketch.quantile(0.5, SearchCriteria::Inclusive).unwrap();
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]