use std::collections::{HashSet, hash_map::RandomState};
use std::hash::{BuildHasher, Hash, Hasher};
#[derive(Clone, Debug)]
pub struct Rendezvous<N, S = RandomState> {
nodes: HashSet<N>,
build: S,
}
impl<N> Rendezvous<N, RandomState>
where
N: Hash + Eq,
{
pub fn new() -> Self {
Self {
nodes: Default::default(),
build: RandomState::new(),
}
}
pub fn from_nodes(nodes: impl IntoIterator<Item = N>) -> Self {
Self {
nodes: nodes.into_iter().collect(),
build: RandomState::new(),
}
}
}
impl<N> Default for Rendezvous<N, RandomState>
where
N: Hash + Eq,
{
fn default() -> Self {
Self::new()
}
}
impl<N, S> Rendezvous<N, S>
where
N: Hash + Eq + Ord + PartialOrd,
S: BuildHasher + Clone,
{
pub fn from_nodes_and_hasher(nodes: impl IntoIterator<Item = N>, build: S) -> Self {
Self {
nodes: nodes.into_iter().collect(),
build,
}
}
pub fn add_node(&mut self, node: N) -> bool {
self.nodes.insert(node)
}
pub fn remove_node(&mut self, node: &N) -> bool {
self.nodes.remove(node)
}
#[inline]
fn hrw_score<K: Hash>(key: &K, node: &N, build: &S) -> u64 {
let mut h = build.build_hasher();
key.hash(&mut h);
node.hash(&mut h);
h.finish()
}
pub fn pick_top<K: Hash>(&self, key: &K) -> Option<&N> {
self.nodes
.iter()
.max_by_key(|&n| (Self::hrw_score(key, n, &self.build), n))
}
pub fn pick_top_k<K: Hash>(&self, key: &K, k: usize) -> Vec<&N> {
if self.nodes.is_empty() || k == 0 {
return Vec::new();
}
let k = k.min(self.nodes.len());
let mut scored: Vec<_> = self
.nodes
.iter()
.map(|n| (Self::hrw_score(key, n, &self.build), n))
.collect();
let k = k.min(scored.len());
let nth = k - 1;
scored.select_nth_unstable_by(nth, |a, b| b.0.cmp(&a.0));
scored[..k].sort_unstable_by(|a, b| b.0.cmp(&a.0));
scored[..k].iter().map(|&(_, n)| n).collect()
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_pick_default_hasher() {
let r = Rendezvous::from_nodes(["A", "B", "C"]);
assert!(r.pick_top(&"key").is_some());
}
#[test]
fn distribution_stability_on_node_change() {
use std::collections::HashMap;
let mut r = Rendezvous::from_nodes(["a", "b", "c", "d"]);
let mut map1 = HashMap::new();
for i in 0..1000 {
map1.insert(i, r.pick_top(&i).unwrap().to_string());
}
r.add_node("e");
let mut map2 = HashMap::new();
let mut moved1_2 = 0;
for i in 0..1000 {
let n = r.pick_top(&i).unwrap().to_string();
if *map1.get(&i).unwrap() != n {
moved1_2 += 1;
}
map2.insert(i, n);
}
r.remove_node(&"c");
let mut map3 = HashMap::new();
let mut moved2_3 = 0;
for i in 0..1000 {
let n = r.pick_top(&i).unwrap().to_string();
if *map2.get(&i).unwrap() != n {
moved2_3 += 1;
}
map3.insert(i, n);
}
assert!(moved1_2 >= 100 && moved1_2 <= 300, "{moved1_2}");
assert!(moved2_3 >= 100 && moved2_3 <= 300, "{moved2_3}");
}
#[test]
fn ahash_pick_top_and_top_k() {
use ahash::RandomState as AHash;
let r = Rendezvous::from_nodes_and_hasher(["X", "Y", "Z"], AHash::new());
let key = "alpha";
let one = r.pick_top(&key).unwrap();
let top2 = r.pick_top_k(&key, 2);
assert_eq!(top2.len(), 2);
assert_eq!(top2[0], one);
}
#[test]
fn top_k_deterministic_order() {
let r = Rendezvous::from_nodes(["A", "B", "C", "D"]);
let p1 = r.pick_top_k(&"k42", 3);
let p2 = r.pick_top_k(&"k42", 3);
assert_eq!(p1, p2);
}
#[test]
fn custom_hasher_construction_compiles() {
let r: Rendezvous<&'static str, RandomState> =
Rendezvous::from_nodes_and_hasher(["n1", "n2", "n3"], RandomState::new());
assert!(r.pick_top(&"abc").is_some());
}
#[test]
fn add_and_remove_nodes() {
let mut r = Rendezvous::new();
assert!(r.is_empty());
r.add_node("A");
r.add_node("B");
assert_eq!(r.len(), 2);
assert!(!r.is_empty());
assert!(r.remove_node(&"A"));
assert_eq!(r.len(), 1);
assert!(!r.remove_node(&"A")); assert!(r.remove_node(&"B"));
assert!(r.is_empty());
}
#[test]
fn no_duplicate_nodes() {
let mut r = Rendezvous::from_nodes(["A"]);
r.add_node("A");
assert_eq!(r.len(), 1);
r.add_node("B");
assert_eq!(r.len(), 2);
}
#[test]
fn pick_top_and_top_k_consistency() {
let r = Rendezvous::from_nodes(["A", "B", "C"]);
let key = "mykey";
let one = r.pick_top(&key);
let mut top1 = r.pick_top_k(&key, 1);
let top1_opt = top1.pop();
if one != top1_opt {
eprintln!("pick_top: {:?}, pick_top_k(1): {:?}", one, top1_opt);
}
assert!(
one.is_none() && top1_opt.is_none() || one == top1_opt,
"pick_top: {:?}, pick_top_k(1): {:?}",
one,
top1_opt
);
}
#[test]
fn pick_top_k_edge_cases() {
let r = Rendezvous::from_nodes(["A", "B"]);
let empty = Rendezvous::<&str>::new();
assert!(empty.pick_top_k(&"k", 1).is_empty());
assert!(r.pick_top_k(&"k", 0).is_empty());
let all = r.pick_top_k(&"k", 10);
assert!(all.len() <= r.len());
for n in &all {
assert!(r.nodes.contains(*n));
}
}
}