use alloc::collections::BinaryHeap;
use alloc::vec::Vec;
pub(crate) struct NearestBound<T> {
ranks: BinaryHeap<Ranked<T>>,
k: usize,
bound: f64,
next_ordinal: usize,
#[cfg(test)]
metrics: NearestBoundMetrics,
}
struct Ranked<T> {
dist: f64,
ordinal: usize,
value: T,
}
impl<T> PartialEq for Ranked<T> {
fn eq(&self, other: &Self) -> bool {
self.dist.total_cmp(&other.dist).is_eq() && self.ordinal == other.ordinal
}
}
impl<T> Eq for Ranked<T> {}
impl<T> PartialOrd for Ranked<T> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T> Ord for Ranked<T> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.dist
.total_cmp(&other.dist)
.then_with(|| self.ordinal.cmp(&other.ordinal))
}
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct NearestBoundMetrics {
pub(crate) calls: usize,
pub(crate) partition_comparisons: usize,
pub(crate) admissions: usize,
pub(crate) replacements: usize,
pub(crate) shifted_ranks: usize,
}
impl<T> NearestBound<T> {
pub(crate) fn new(k: usize, capacity: usize) -> Self {
Self {
ranks: BinaryHeap::with_capacity(capacity),
k,
bound: f64::INFINITY,
next_ordinal: 0,
#[cfg(test)]
metrics: NearestBoundMetrics::default(),
}
}
pub(crate) fn bound(&self) -> f64 {
self.bound
}
pub(crate) fn admit_better(&mut self, dist: f64, value: T) {
debug_assert!(dist.total_cmp(&self.bound()).is_lt());
let rank = Ranked {
dist,
ordinal: self.next_ordinal,
value,
};
self.next_ordinal += 1;
if self.ranks.len() == self.k {
#[cfg(test)]
{
self.metrics.replacements += 1;
}
*self
.ranks
.peek_mut()
.expect("a full positive-k heap has a greatest rank") = rank;
} else {
self.ranks.push(rank);
}
if self.ranks.len() == self.k {
self.bound = self
.ranks
.peek()
.expect("a full positive-k heap has a greatest rank")
.dist;
}
#[cfg(test)]
{
self.metrics.calls += 1;
self.metrics.admissions += 1;
}
}
pub(crate) fn into_values(self) -> Vec<T> {
self.ranks
.into_sorted_vec()
.into_iter()
.map(|rank| rank.value)
.collect()
}
#[cfg(test)]
pub(crate) fn metrics(&self) -> NearestBoundMetrics {
self.metrics
}
}
#[cfg(test)]
#[allow(clippy::float_cmp, reason = "exact rank distances, no arithmetic")]
mod tests {
use super::{NearestBound, Ranked};
#[test]
fn ranked_equality_uses_distance_and_admission_order() {
let first = Ranked {
dist: 1.0,
ordinal: 2,
value: (),
};
let same = Ranked {
dist: 1.0,
ordinal: 2,
value: (),
};
let later = Ranked {
dist: 1.0,
ordinal: 3,
value: (),
};
assert!(first == same);
assert!(first != later);
}
#[test]
fn unfull_bound_is_infinity() {
let mut bound = NearestBound::new(3, 3);
assert_eq!(bound.bound(), f64::INFINITY);
bound.admit_better(4.0, 1u32);
bound.admit_better(1.0, 2);
assert_eq!(bound.bound(), f64::INFINITY);
bound.admit_better(9.0, 3);
assert_eq!(bound.bound(), 9.0);
assert_eq!(bound.into_values(), [2, 1, 3]);
}
#[test]
fn better_candidate_replaces_the_bound() {
let mut bound = NearestBound::new(3, 3);
bound.admit_better(1.0, 1u32);
bound.admit_better(2.0, 2);
bound.admit_better(3.0, 3);
assert_eq!(bound.bound(), 3.0);
bound.admit_better(2.5, 25);
assert_eq!(bound.bound(), 2.5);
assert_eq!(bound.into_values(), [1, 2, 25]);
}
#[test]
fn ties_at_the_k_boundary_keep_the_first_admitted() {
let mut bound = NearestBound::new(2, 2);
bound.admit_better(5.0, 1u32);
bound.admit_better(5.0, 2);
assert!(5.0f64.total_cmp(&bound.bound()).is_ge());
assert_eq!(bound.bound(), 5.0);
bound.admit_better(1.0, 4);
assert_eq!(bound.bound(), 5.0);
assert_eq!(bound.into_values(), [4, 1]);
}
#[test]
fn k_one_tracks_the_single_best() {
let mut bound = NearestBound::new(1, 1);
bound.admit_better(5.0, 1u32);
assert_eq!(bound.bound(), 5.0);
assert!(7.0f64.total_cmp(&bound.bound()).is_ge());
bound.admit_better(2.0, 3);
assert_eq!(bound.bound(), 2.0);
assert_eq!(bound.into_values(), [3]);
}
}