298 lines
9.6 KiB
Rust
298 lines
9.6 KiB
Rust
use log::trace;
|
|
use std::any::type_name;
|
|
use std::fmt::Debug;
|
|
use std::ops::Deref;
|
|
use std::time::Instant;
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct StateEntry<T> {
|
|
state: T,
|
|
valid_until: Instant,
|
|
priority: u8,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CommandedState<T, const N: usize = 2> {
|
|
entries: [Option<StateEntry<T>>; N],
|
|
default: T,
|
|
changed: bool,
|
|
}
|
|
|
|
impl<T, const N: usize> CommandedState<T, N>
|
|
where
|
|
T: Debug,
|
|
{
|
|
pub fn new(default: T) -> Self {
|
|
trace!(
|
|
"CommandedState::<{}>::new(default: {default:?})",
|
|
type_name::<T>()
|
|
);
|
|
Self {
|
|
entries: [const { None }; N],
|
|
default,
|
|
changed: true,
|
|
}
|
|
}
|
|
|
|
pub fn consume_changed(&mut self) -> bool {
|
|
trace!(
|
|
"CommandedState::<{}>::consume_changed(self: {self:?})",
|
|
type_name::<T>()
|
|
);
|
|
let res = self.changed;
|
|
self.changed = false;
|
|
res
|
|
}
|
|
|
|
fn evaluate_change_if<F>(&mut self, current_time: Instant, f: F) -> &T
|
|
where
|
|
F: FnOnce(&T, &T) -> bool,
|
|
{
|
|
trace!(
|
|
"CommandedState::<{}>::evaluate_change_if(self: {self:?}, current_time: {current_time:?}, f: {})",
|
|
type_name::<T>(),
|
|
type_name::<F>()
|
|
);
|
|
let mut original_value: Option<StateEntry<T>> = None;
|
|
for i in 0..N {
|
|
if let Some(entry) = &self.entries[0] {
|
|
if entry.valid_until >= current_time {
|
|
if let Some(original_value) = original_value {
|
|
self.changed |= f(&original_value.state, &entry.state);
|
|
}
|
|
// SAFETY: we just checked that this index is Some
|
|
// Unfortunately we can't use entry in order to satisfy the borrow checker
|
|
return self.entries[0].as_ref().map(|x| &x.state).unwrap();
|
|
}
|
|
// An expired entry - we need to remove it
|
|
|
|
// Empty out this entry (keeping it in original value in case it is needed)
|
|
if i == 0 {
|
|
original_value = self.entries[0].take();
|
|
} else {
|
|
self.entries[0] = None;
|
|
}
|
|
// Bump up all future entries
|
|
self.entries.rotate_left(1);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if let Some(original_value) = original_value {
|
|
self.changed |= f(&original_value.state, &self.default);
|
|
}
|
|
&self.default
|
|
}
|
|
|
|
fn insert_change_if<F>(&mut self, state: T, valid_until: Instant, priority: u8, f: F)
|
|
where
|
|
F: FnOnce(&T, &T) -> bool,
|
|
{
|
|
trace!(
|
|
"CommandedState::<{}>::insert_change_if(self: {self:?}, state: {state:?}, valid_until: {valid_until:?}, priority: {priority}, f: {})",
|
|
type_name::<T>(),
|
|
type_name::<F>()
|
|
);
|
|
for i in 0..N {
|
|
if let Some(entry) = &mut self.entries[i] {
|
|
// The current entry exists - let's find out it replacing it is an option
|
|
if priority >= entry.priority {
|
|
// We have enough priority to modify this entry
|
|
if priority == entry.priority || entry.valid_until <= valid_until {
|
|
// If we are same priority (always replace) or
|
|
// we will be valid for longer then we can just replace this entry
|
|
if i == 0 {
|
|
self.changed |= f(&state, &entry.state);
|
|
}
|
|
*entry = StateEntry {
|
|
state,
|
|
valid_until,
|
|
priority,
|
|
};
|
|
// Remove entries which will expire before this one
|
|
for j in (i + 1)..N {
|
|
if let Some(later_entry) = &self.entries[j] {
|
|
// If the later entry exists and expires before this one
|
|
if later_entry.valid_until <= valid_until {
|
|
self.entries[j] = None;
|
|
self.entries[j..].rotate_left(1);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// We want to add a higher priority entry, but we will expire first
|
|
if i == 0 {
|
|
self.changed |= f(&state, &entry.state);
|
|
}
|
|
self.entries[i..].rotate_right(1);
|
|
self.entries[i] = Some(StateEntry {
|
|
state,
|
|
valid_until,
|
|
priority,
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
// No entry exists so we can just put ourselves there
|
|
if i == 0 {
|
|
self.changed |= f(&state, &self.default);
|
|
}
|
|
self.entries[i] = Some(StateEntry {
|
|
state,
|
|
valid_until,
|
|
priority,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
// If we got here then we didn't find a valid place to insert it
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[inline]
|
|
pub fn evaluate_changed(&mut self, current_time: Instant) -> &T {
|
|
self.evaluate_change_if(current_time, |_, _| true)
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#[inline]
|
|
pub fn insert_changed(&mut self, state: T, valid_until: Instant, priority: u8) {
|
|
self.insert_change_if(state, valid_until, priority, |_, _| true);
|
|
}
|
|
}
|
|
|
|
impl<T, const N: usize> CommandedState<T, N>
|
|
where
|
|
T: PartialEq + Debug,
|
|
{
|
|
#[inline]
|
|
pub fn evaluate(&mut self, current_time: Instant) -> &T {
|
|
self.evaluate_change_if(current_time, T::ne)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn insert(&mut self, state: T, valid_until: Instant, priority: u8) {
|
|
self.insert_change_if(state, valid_until, priority, T::ne);
|
|
}
|
|
}
|
|
|
|
impl<T, const N: usize> Deref for CommandedState<T, N> {
|
|
type Target = T;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
for i in 0..N {
|
|
if let Some(entry) = &self.entries[i] {
|
|
return &entry.state;
|
|
}
|
|
}
|
|
&self.default
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use log::LevelFilter;
|
|
use std::time::Duration;
|
|
|
|
#[test]
|
|
fn commanded_state() {
|
|
let _ = env_logger::builder()
|
|
.is_test(true)
|
|
.filter_level(LevelFilter::Trace)
|
|
.try_init();
|
|
|
|
let mut state = CommandedState::<_, 2>::new(4);
|
|
let now = Instant::now();
|
|
assert!(!state.consume_changed());
|
|
assert_eq!(4, *state.evaluate(now));
|
|
assert!(!state.consume_changed());
|
|
|
|
state.insert(5, now, 0);
|
|
assert!(state.consume_changed());
|
|
assert!(!state.consume_changed());
|
|
assert_eq!(5, *state.evaluate(now));
|
|
assert!(!state.consume_changed());
|
|
|
|
let now = now + Duration::from_secs(1);
|
|
assert_eq!(4, *state.evaluate(now));
|
|
assert!(state.consume_changed());
|
|
|
|
state.insert(6, now + Duration::from_secs(2), 0);
|
|
assert!(state.consume_changed());
|
|
assert_eq!(6, *state.evaluate(now));
|
|
assert!(!state.consume_changed());
|
|
|
|
state.insert(7, now + Duration::from_secs(1), 1);
|
|
assert!(state.consume_changed());
|
|
assert_eq!(7, *state);
|
|
|
|
state.insert(8, now + Duration::from_secs(3), 0);
|
|
assert!(!state.consume_changed());
|
|
assert_eq!(7, *state);
|
|
|
|
state.insert(9, now + Duration::from_secs(2), 1);
|
|
assert!(state.consume_changed());
|
|
assert_eq!(9, *state);
|
|
|
|
assert_eq!(9, *state.evaluate(now));
|
|
assert!(!state.consume_changed());
|
|
|
|
let now = now + Duration::from_secs(2);
|
|
assert_eq!(9, *state.evaluate(now));
|
|
assert!(!state.consume_changed());
|
|
|
|
let now = now + Duration::from_secs(1);
|
|
assert_eq!(8, *state.evaluate(now));
|
|
assert!(state.consume_changed());
|
|
|
|
let now = now + Duration::from_secs(1);
|
|
assert_eq!(4, *state.evaluate(now));
|
|
assert!(state.consume_changed());
|
|
|
|
state.insert(10, now + Duration::from_secs(1), 0);
|
|
state.insert(11, now, 1);
|
|
state.insert(12, now + Duration::from_secs(1), 1);
|
|
assert!(state.consume_changed());
|
|
assert_eq!(12, *state);
|
|
|
|
let now = now + Duration::from_secs(2);
|
|
assert_eq!(4, *state.evaluate(now));
|
|
assert!(state.consume_changed());
|
|
|
|
state.insert(13, now + Duration::from_secs(1), 0);
|
|
state.insert(14, now, 1);
|
|
assert_eq!(14, *state);
|
|
let now = now + Duration::from_secs(2);
|
|
assert_eq!(4, *state.evaluate(now));
|
|
assert!(state.consume_changed());
|
|
}
|
|
|
|
#[test]
|
|
fn changed() {
|
|
let _ = env_logger::builder()
|
|
.is_test(true)
|
|
.filter_level(LevelFilter::Trace)
|
|
.try_init();
|
|
|
|
let mut state = CommandedState::<_, 2>::new(4);
|
|
let now = Instant::now();
|
|
assert!(!state.consume_changed());
|
|
assert_eq!(4, *state);
|
|
|
|
state.insert(4, now + Duration::from_secs(1), 0);
|
|
assert!(!state.consume_changed());
|
|
|
|
let now = Instant::now() + Duration::from_secs(2);
|
|
assert_eq!(4, *state.evaluate_changed(now));
|
|
assert!(state.consume_changed());
|
|
|
|
state.insert_changed(4, now + Duration::from_secs(1), 0);
|
|
assert!(state.consume_changed());
|
|
|
|
let now = Instant::now() + Duration::from_secs(2);
|
|
assert_eq!(4, *state.evaluate(now));
|
|
}
|
|
}
|