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 e8b881c  feat(frequencies): add borrowed-key update methods to the 
frequent items sketch (#140)
e8b881c is described below

commit e8b881c50a3b8b3ae9e0798afeec230ce72172e7
Author: kedom <[email protected]>
AuthorDate: Thu Jul 2 04:50:36 2026 +0200

    feat(frequencies): add borrowed-key update methods to the frequent items 
sketch (#140)
    
    Signed-off-by: kedom <[email protected]>
    Signed-off-by: tison <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
    Co-authored-by: tison <[email protected]>
---
 .../src/frequencies/reverse_purge_item_hash_map.rs | 60 ++++++++++++-------
 datasketches/src/frequencies/sketch.rs             | 70 +++++++++++++++++++++-
 datasketches/tests/frequencies_update_test.rs      | 28 +++++++++
 3 files changed, 134 insertions(+), 24 deletions(-)

diff --git a/datasketches/src/frequencies/reverse_purge_item_hash_map.rs 
b/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
index 79ed290..0e59745 100644
--- a/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
+++ b/datasketches/src/frequencies/reverse_purge_item_hash_map.rs
@@ -20,6 +20,7 @@
 //! This linear-probing hash map supports a reverse purge operation that 
removes
 //! keys with non-positive counts by scanning clusters from the back to the 
front.
 
+use std::borrow::Borrow;
 use std::hash::Hash;
 use std::hash::Hasher;
 
@@ -59,8 +60,15 @@ impl<T: Eq + Hash> ReversePurgeItemHashMap<T> {
     }
 
     /// Returns the value for `key`, or zero if the key is not present.
-    pub fn get(&self, key: &T) -> u64 {
-        let probe = self.hash_probe(key);
+    ///
+    /// The key may be any borrowed form of `T` (e.g. `&str` for a `String` 
map),
+    /// matching the `HashMap::get` convention.
+    pub fn get<Q>(&self, key: &Q) -> u64
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ?Sized,
+    {
+        let (probe, _) = self.find_probe_or_empty(key);
         if self.states[probe] > 0 {
             return self.values[probe];
         }
@@ -69,21 +77,7 @@ impl<T: Eq + Hash> ReversePurgeItemHashMap<T> {
 
     /// Adds `adjust_amount` to the value for `key`, inserting if absent.
     pub fn adjust_or_put_value(&mut self, key: T, adjust_amount: u64) {
-        let mask = self.keys.len() - 1;
-        let mut probe = (hash_item(&key) as usize) & mask;
-        let mut drift: usize = 1;
-        while self.states[probe] != 0 {
-            let matches = self.keys[probe]
-                .as_ref()
-                .map(|existing| existing == &key)
-                .unwrap_or(false);
-            if matches {
-                break;
-            }
-            probe = (probe + 1) & mask;
-            drift += 1;
-            debug_assert!(drift < DRIFT_LIMIT, "drift limit exceeded");
-        }
+        let (probe, drift) = self.find_probe_or_empty(&key);
         if self.states[probe] == 0 {
             self.keys[probe] = Some(key);
             self.values[probe] = adjust_amount;
@@ -94,6 +88,23 @@ impl<T: Eq + Hash> ReversePurgeItemHashMap<T> {
         }
     }
 
+    /// Adds `adjust_amount` to the value for a borrowed `key`, inserting if 
absent.
+    pub fn adjust_or_put_value_ref<Q>(&mut self, key: &Q, adjust_amount: u64)
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ToOwned<Owned = T> + ?Sized,
+    {
+        let (probe, drift) = self.find_probe_or_empty(key);
+        if self.states[probe] == 0 {
+            self.keys[probe] = Some(key.to_owned());
+            self.values[probe] = adjust_amount;
+            self.states[probe] = drift as u16;
+            self.num_active += 1;
+        } else {
+            self.values[probe] += adjust_amount;
+        }
+    }
+
     /// Removes all keys with non-positive counts.
     fn keep_only_positive_counts(&mut self) {
         let len = self.keys.len();
@@ -228,20 +239,27 @@ impl<T: Eq + Hash> ReversePurgeItemHashMap<T> {
         self.states[probe] > 0
     }
 
-    fn hash_probe(&self, key: &T) -> usize {
+    fn find_probe_or_empty<Q>(&self, key: &Q) -> (usize, usize)
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ?Sized,
+    {
         let mask = self.keys.len() - 1;
         let mut probe = (hash_item(key) as usize) & mask;
+        let mut drift: usize = 1;
         while self.states[probe] > 0 {
             let matches = self.keys[probe]
                 .as_ref()
-                .map(|existing| existing == key)
+                .map(|existing| existing.borrow() == key)
                 .unwrap_or(false);
             if matches {
                 break;
             }
             probe = (probe + 1) & mask;
+            drift += 1;
+            debug_assert!(drift < DRIFT_LIMIT, "drift limit exceeded");
         }
-        probe
+        (probe, drift)
     }
 
     fn hash_delete(&mut self, mut delete_probe: usize) {
@@ -312,7 +330,7 @@ impl<'a, T> Iterator for ReversePurgeItemIter<'a, T> {
 }
 
 #[inline]
-fn hash_item<T: Hash>(item: &T) -> u64 {
+fn hash_item<T: Hash + ?Sized>(item: &T) -> u64 {
     let mut hasher = MurmurHash3X64128::default();
     item.hash(&mut hasher);
     hasher.finish()
diff --git a/datasketches/src/frequencies/sketch.rs 
b/datasketches/src/frequencies/sketch.rs
index fc2b297..f46a231 100644
--- a/datasketches/src/frequencies/sketch.rs
+++ b/datasketches/src/frequencies/sketch.rs
@@ -17,6 +17,7 @@
 
 //! Frequent items sketch implementations.
 
+use std::borrow::Borrow;
 use std::hash::Hash;
 
 use crate::codec::SketchBytes;
@@ -158,7 +159,11 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
     /// sketch.update_with_count(10, 2);
     /// assert!(sketch.estimate(&10) >= 2);
     /// ```
-    pub fn estimate(&self, item: &T) -> u64 {
+    pub fn estimate<Q>(&self, item: &Q) -> u64
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ?Sized,
+    {
         let value = self.hash_map.get(item);
         if value > 0 { value + self.offset } else { 0 }
     }
@@ -167,7 +172,11 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
     ///
     /// This value is guaranteed to be no larger than the true frequency. If 
the item is not
     /// tracked, the lower bound is zero.
-    pub fn lower_bound(&self, item: &T) -> u64 {
+    pub fn lower_bound<Q>(&self, item: &Q) -> u64
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ?Sized,
+    {
         self.hash_map.get(item)
     }
 
@@ -175,7 +184,11 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
     ///
     /// This value is guaranteed to be no smaller than the true frequency. If 
the item is tracked,
     /// this is `item_count + offset`.
-    pub fn upper_bound(&self, item: &T) -> u64 {
+    pub fn upper_bound<Q>(&self, item: &Q) -> u64
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ?Sized,
+    {
         self.hash_map.get(item) + self.offset
     }
 
@@ -263,6 +276,57 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
         self.maybe_resize_or_purge();
     }
 
+    /// Updates the sketch with a borrowed item and a count of one.
+    ///
+    /// Equivalent to [`update`](Self::update) but takes the item by reference 
and
+    /// only allocates an owned item when it is newly inserted, so updating an
+    /// already-tracked item is allocation free.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use datasketches::frequencies::FrequentItemsSketch;
+    /// let mut sketch = FrequentItemsSketch::<String>::new(64);
+    /// sketch.update_ref("nginx");
+    /// sketch.update_ref("nginx"); // no allocation on the second hit
+    /// assert!(sketch.estimate("nginx") >= 2);
+    /// ```
+    pub fn update_ref<Q>(&mut self, item: &Q)
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ToOwned<Owned = T> + ?Sized,
+    {
+        self.update_with_count_ref(item, 1);
+    }
+
+    /// Updates the sketch with a borrowed item and count.
+    ///
+    /// Equivalent to [`update_with_count`](Self::update_with_count) but takes 
the
+    /// item by reference and only allocates an owned item when it is newly
+    /// inserted. A count of zero is a no-op.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use datasketches::frequencies::FrequentItemsSketch;
+    /// let mut sketch = FrequentItemsSketch::<String>::new(64);
+    /// sketch.update_with_count_ref("gzip", 3);
+    /// assert!(sketch.estimate("gzip") >= 3);
+    /// ```
+    pub fn update_with_count_ref<Q>(&mut self, item: &Q, count: u64)
+    where
+        T: Borrow<Q>,
+        Q: Eq + Hash + ToOwned<Owned = T> + ?Sized,
+    {
+        if count == 0 {
+            return;
+        }
+        assert!(count > 0, "count may not be negative");
+        self.stream_weight += count;
+        self.hash_map.adjust_or_put_value_ref(item, count);
+        self.maybe_resize_or_purge();
+    }
+
     /// Merges another sketch into this one.
     ///
     /// The other sketch may have a different map size. The merged sketch 
respects the
diff --git a/datasketches/tests/frequencies_update_test.rs 
b/datasketches/tests/frequencies_update_test.rs
index f15f219..87c702a 100644
--- a/datasketches/tests/frequencies_update_test.rs
+++ b/datasketches/tests/frequencies_update_test.rs
@@ -119,6 +119,34 @@ fn test_items_one_item() {
     assert_eq!(sketch.upper_bound(&item), 1);
 }
 
+#[test]
+fn test_items_borrowed_key_updates_and_queries() {
+    let mut sketch = FrequentItemsSketch::<String>::new(16);
+
+    sketch.update_ref("alpha");
+    sketch.update_ref("alpha");
+    sketch.update_with_count_ref("beta", 3);
+    sketch.update_with_count_ref("ignored", 0);
+    for item in [
+        "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", 
"lambda", "mu",
+    ] {
+        sketch.update_ref(item);
+    }
+
+    assert!(!sketch.is_empty());
+    assert_eq!(sketch.total_weight(), 15);
+    assert_eq!(sketch.num_active_items(), 12);
+    assert_eq!(sketch.estimate("alpha"), 2);
+    assert_eq!(sketch.lower_bound("alpha"), 2);
+    assert_eq!(sketch.upper_bound("alpha"), 2);
+    assert_eq!(sketch.estimate("beta"), 3);
+    assert_eq!(sketch.estimate("ignored"), 0);
+    assert_eq!(sketch.estimate("missing"), 0);
+
+    let owned = "alpha".to_string();
+    assert_eq!(sketch.estimate(&owned), 2);
+}
+
 #[test]
 fn test_longs_several_items_no_resize_no_purge() {
     let mut sketch: FrequentItemsSketch<i64> = FrequentItemsSketch::new(8);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to