use std::collections::HashMap;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mutation {
Changed,
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Move {
Absent,
Unchanged,
Reordered,
}
impl Move {
pub(crate) fn is_present(self) -> bool {
!matches!(self, Move::Absent)
}
pub(crate) fn changed(self) -> bool {
matches!(self, Move::Reordered)
}
}
pub(crate) struct KeyedOrder<K, H> {
entries: HashMap<K, H>,
order: Vec<K>,
}
impl<K, H> Default for KeyedOrder<K, H> {
fn default() -> Self {
Self {
entries: HashMap::new(),
order: Vec::new(),
}
}
}
impl<K, H> KeyedOrder<K, H>
where
K: Eq + Hash + Clone,
H: Copy,
{
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn get(&self, key: &K) -> Option<H> {
self.entries.get(key).copied()
}
pub(crate) fn contains(&self, key: &K) -> bool {
self.entries.contains_key(key)
}
pub(crate) fn insert(&mut self, key: K, handle: H) -> (H, Mutation) {
if let Some(existing) = self.entries.get(&key) {
return (*existing, Mutation::Unchanged);
}
self.entries.insert(key.clone(), handle);
self.order.push(key);
(handle, Mutation::Changed)
}
pub(crate) fn remove(&mut self, key: &K) -> (Option<H>, Mutation) {
let Some(handle) = self.entries.remove(key) else {
return (None, Mutation::Unchanged);
};
self.order.retain(|k| k != key);
(Some(handle), Mutation::Changed)
}
pub(crate) fn keys(&self) -> Vec<K> {
self.order.clone()
}
pub(crate) fn len(&self) -> usize {
self.order.len()
}
pub(crate) fn position(&self, key: &K) -> Option<usize> {
self.order.iter().position(|k| k == key)
}
pub(crate) fn move_to(&mut self, key: &K, index: usize) -> Move {
let Some(from) = self.position(key) else {
return Move::Absent;
};
let to = index.min(self.order.len().saturating_sub(1));
if from == to {
return Move::Unchanged;
}
let k = self.order.remove(from);
self.order.insert(to, k);
Move::Reordered
}
pub(crate) fn move_before(&mut self, key: &K, anchor: &K) -> Move {
let (Some(anchor_idx), Some(from)) = (self.position(anchor), self.position(key)) else {
return Move::Absent;
};
let target = if from < anchor_idx {
anchor_idx - 1
} else {
anchor_idx
};
self.move_to(key, target)
}
pub(crate) fn move_after(&mut self, key: &K, anchor: &K) -> Move {
let (Some(anchor_idx), Some(from)) = (self.position(anchor), self.position(key)) else {
return Move::Absent;
};
let target = if from <= anchor_idx {
anchor_idx
} else {
anchor_idx + 1
};
self.move_to(key, target)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seeded(keys: &[&str]) -> KeyedOrder<String, u32> {
let mut core = KeyedOrder::new();
for (i, key) in keys.iter().enumerate() {
core.insert((*key).to_string(), i as u32);
}
core
}
fn k(s: &str) -> String {
s.to_string()
}
#[test]
fn insert_appends_and_reports_growth() {
let mut core = KeyedOrder::new();
assert_eq!(core.insert(k("a"), 1), (1, Mutation::Changed));
assert_eq!(core.insert(k("b"), 2), (2, Mutation::Changed));
assert_eq!(core.keys(), vec![k("a"), k("b")]);
assert_eq!(core.len(), 2);
}
#[test]
fn reinsert_keeps_the_original_handle_and_reports_no_change() {
let mut core = seeded(&["a"]);
assert_eq!(core.insert(k("a"), 99), (0, Mutation::Unchanged));
assert_eq!(core.get(&k("a")), Some(0));
assert_eq!(core.keys(), vec![k("a")]);
}
#[test]
fn remove_clears_both_planes() {
let mut core = seeded(&["a", "b", "c"]);
let (handle, mutation) = core.remove(&k("b"));
assert_eq!((handle, mutation), (Some(1), Mutation::Changed));
assert_eq!(core.keys(), vec![k("a"), k("c")]);
assert!(!core.contains(&k("b")));
assert_eq!(core.position(&k("b")), None);
assert_eq!(core.remove(&k("b")), (None, Mutation::Unchanged));
}
#[test]
fn move_to_reorders_and_clamps() {
let mut core = seeded(&["a", "b", "c", "d"]);
assert_eq!(core.move_to(&k("b"), 3), Move::Reordered);
assert_eq!(core.keys(), vec![k("a"), k("c"), k("d"), k("b")]);
assert_eq!(core.move_to(&k("a"), 99), Move::Reordered);
assert_eq!(core.keys(), vec![k("c"), k("d"), k("b"), k("a")]);
}
#[test]
fn move_to_same_position_is_unchanged_not_reordered() {
let mut core = seeded(&["a", "b", "c"]);
assert_eq!(core.move_to(&k("b"), 1), Move::Unchanged);
assert!(core.move_to(&k("b"), 1).is_present());
assert!(!core.move_to(&k("b"), 1).changed());
assert_eq!(core.keys(), vec![k("a"), k("b"), k("c")]);
}
#[test]
fn move_absent_key_is_absent() {
let mut core = seeded(&["a"]);
assert_eq!(core.move_to(&k("zz"), 0), Move::Absent);
assert!(!core.move_to(&k("zz"), 0).is_present());
assert_eq!(core.move_before(&k("zz"), &k("a")), Move::Absent);
assert_eq!(core.move_before(&k("a"), &k("zz")), Move::Absent);
assert_eq!(core.move_after(&k("a"), &k("zz")), Move::Absent);
}
#[test]
fn move_before_lands_immediately_ahead_of_the_anchor_from_either_side() {
let mut core = seeded(&["a", "b", "c", "d"]);
assert_eq!(core.move_before(&k("a"), &k("d")), Move::Reordered);
assert_eq!(core.keys(), vec![k("b"), k("c"), k("a"), k("d")]);
let mut core = seeded(&["a", "b", "c", "d"]);
assert_eq!(core.move_before(&k("d"), &k("a")), Move::Reordered);
assert_eq!(core.keys(), vec![k("d"), k("a"), k("b"), k("c")]);
}
#[test]
fn move_after_lands_immediately_behind_the_anchor_from_either_side() {
let mut core = seeded(&["a", "b", "c", "d"]);
assert_eq!(core.move_after(&k("a"), &k("c")), Move::Reordered);
assert_eq!(core.keys(), vec![k("b"), k("c"), k("a"), k("d")]);
let mut core = seeded(&["a", "b", "c", "d"]);
assert_eq!(core.move_after(&k("d"), &k("a")), Move::Reordered);
assert_eq!(core.keys(), vec![k("a"), k("d"), k("b"), k("c")]);
}
#[test]
fn move_relative_to_an_adjacent_anchor_is_a_no_op() {
let mut core = seeded(&["a", "b", "c"]);
assert_eq!(core.move_before(&k("a"), &k("b")), Move::Unchanged);
assert_eq!(core.move_after(&k("c"), &k("b")), Move::Unchanged);
assert_eq!(core.keys(), vec![k("a"), k("b"), k("c")]);
}
#[test]
fn entries_and_order_never_desync() {
let mut core = seeded(&["a", "b", "c", "d", "e"]);
core.move_to(&k("e"), 0);
core.remove(&k("c"));
core.move_before(&k("a"), &k("e"));
core.insert(k("f"), 5);
core.move_after(&k("b"), &k("f"));
let keys = core.keys();
assert_eq!(keys.len(), core.len());
for key in &keys {
assert!(core.contains(key), "`{key}` in order but not in entries");
assert!(core.position(key).is_some());
}
for key in ["a", "b", "d", "e", "f"] {
assert!(
keys.contains(&k(key)),
"`{key}` in entries but not in order"
);
}
assert!(!core.contains(&k("c")));
}
}