use core::hash::Hash;
#[derive(Debug, Clone)]
pub struct TopK<K: Eq + Hash + Clone, const CAP: usize> {
entries: [Option<Entry<K>>; CAP],
len: usize,
total: u64,
}
#[derive(Debug, Clone)]
struct Entry<K> {
key: K,
count: u64,
}
impl<K: Eq + Hash + Clone, const CAP: usize> TopK<K, CAP> {
#[inline]
pub fn new() -> Self {
assert!(CAP > 0, "TopK capacity must be > 0");
Self {
entries: core::array::from_fn(|_| None),
len: 0,
total: 0,
}
}
#[inline]
pub fn update(&mut self, key: K) {
self.total += 1;
for e in self.entries[..self.len].iter_mut().flatten() {
if e.key == key {
e.count += 1;
return;
}
}
if self.len < CAP {
self.entries[self.len] = Some(Entry { key, count: 1 });
self.len += 1;
return;
}
let mut min_idx = 0;
let mut min_count = u64::MAX;
for (i, entry) in self.entries.iter().enumerate() {
if let Some(e) = entry {
if e.count < min_count {
min_count = e.count;
min_idx = i;
}
}
}
self.entries[min_idx] = Some(Entry {
key,
count: min_count + 1,
});
}
#[inline]
pub fn top(&self, buf: &mut [(K, u64)]) -> usize {
let n = self.len.min(buf.len());
let mut count = 0;
for e in self.entries[..self.len].iter().flatten() {
if count < n {
buf[count] = (e.key.clone(), e.count);
count += 1;
}
}
buf[..count].sort_unstable_by(|a, b| b.1.cmp(&a.1));
count
}
#[inline]
#[must_use]
pub fn count_of(&self, key: &K) -> u64 {
for e in self.entries[..self.len].iter().flatten() {
if e.key == *key {
return e.count;
}
}
0
}
#[inline]
#[must_use]
pub fn total(&self) -> u64 {
self.total
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[inline]
pub fn reset(&mut self) {
for entry in &mut self.entries {
*entry = None;
}
self.len = 0;
self.total = 0;
}
}
impl<K: Eq + Hash + Clone, const CAP: usize> Default for TopK<K, CAP> {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let tk: TopK<u32, 5> = TopK::new();
assert_eq!(tk.total(), 0);
assert_eq!(tk.len(), 0);
assert!(tk.is_empty());
}
#[test]
fn tracks_single_key() {
let mut tk: TopK<&str, 5> = TopK::new();
tk.update("BTC");
tk.update("BTC");
tk.update("BTC");
assert_eq!(tk.count_of(&"BTC"), 3);
assert_eq!(tk.total(), 3);
}
#[test]
fn tracks_multiple_keys() {
let mut tk: TopK<&str, 5> = TopK::new();
tk.update("BTC");
tk.update("ETH");
tk.update("BTC");
tk.update("SOL");
tk.update("BTC");
tk.update("ETH");
assert_eq!(tk.count_of(&"BTC"), 3);
assert_eq!(tk.count_of(&"ETH"), 2);
assert_eq!(tk.count_of(&"SOL"), 1);
assert_eq!(tk.total(), 6);
}
#[test]
fn top_returns_sorted() {
let mut tk: TopK<&str, 5> = TopK::new();
tk.update("SOL");
for _ in 0..5 {
tk.update("BTC");
}
for _ in 0..3 {
tk.update("ETH");
}
let mut buf = [("", 0u64); 5];
let n = tk.top(&mut buf);
assert_eq!(n, 3);
assert_eq!(buf[0].0, "BTC");
assert_eq!(buf[1].0, "ETH");
assert_eq!(buf[2].0, "SOL");
}
#[test]
fn eviction_replaces_minimum() {
let mut tk: TopK<u32, 3> = TopK::new();
tk.update(1); tk.update(2); tk.update(3);
tk.update(1);
tk.update(4);
assert!(tk.count_of(&1) >= 2);
assert!(tk.count_of(&4) >= 1);
assert_eq!(tk.len(), 3); }
#[test]
fn overcount_property() {
let mut tk: TopK<u32, 2> = TopK::new();
tk.update(1); tk.update(2); tk.update(3);
assert_eq!(tk.count_of(&3), 2);
}
#[test]
fn unknown_key_returns_zero() {
let tk: TopK<u32, 5> = TopK::new();
assert_eq!(tk.count_of(&42), 0);
}
#[test]
fn reset_clears_all() {
let mut tk: TopK<u32, 5> = TopK::new();
tk.update(1);
tk.update(2);
tk.reset();
assert_eq!(tk.total(), 0);
assert_eq!(tk.len(), 0);
assert!(tk.is_empty());
assert_eq!(tk.count_of(&1), 0);
}
#[test]
fn string_keys() {
let mut tk: TopK<u64, 10> = TopK::new();
for i in 0..100 {
tk.update(i % 10);
}
assert_eq!(tk.total(), 100);
for i in 0..10 {
assert_eq!(tk.count_of(&i), 10);
}
}
}