use std::collections::VecDeque;
use jiff::Zoned;
const CAPACITY: usize = 200;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
pub at: Zoned,
pub source: &'static str,
pub text: String,
}
impl Event {
pub fn new(source: &'static str, text: impl Into<String>) -> Self {
Self {
at: Zoned::now(),
source,
text: text.into(),
}
}
}
#[derive(Debug)]
pub struct WatchLog {
entries: VecDeque<Event>,
since: Zoned,
seen_at: Option<Zoned>,
}
impl Default for WatchLog {
fn default() -> Self {
Self {
entries: VecDeque::new(),
since: Zoned::now(),
seen_at: None,
}
}
}
impl WatchLog {
pub fn push(&mut self, event: Event) {
self.entries.push_front(event);
while self.entries.len() > CAPACITY {
self.entries.pop_back();
}
}
pub fn entries(&self) -> impl Iterator<Item = &Event> {
self.entries.iter()
}
pub fn since(&self) -> &Zoned {
&self.since
}
pub fn mark_seen(&mut self) {
self.seen_at = Some(Zoned::now());
}
pub fn unseen(&self) -> Option<usize> {
let seen_at = self.seen_at.as_ref()?;
let fresh = self
.entries
.iter()
.take_while(|entry| &entry.at > seen_at)
.count();
(fresh > 0 && fresh < self.entries.len()).then_some(fresh)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::Span;
fn at(log: &WatchLog, minutes: i64) -> Event {
Event {
at: log
.since
.checked_add(Span::new().minutes(minutes))
.expect("in range"),
source: "test",
text: format!("thing {minutes}"),
}
}
#[test]
fn the_newest_entry_comes_first() {
let mut log = WatchLog::default();
log.push(at(&log, 1));
log.push(at(&log, 2));
let texts: Vec<&str> = log.entries().map(|e| e.text.as_str()).collect();
assert_eq!(texts, ["thing 2", "thing 1"]);
}
#[test]
fn the_oldest_entries_fall_off_rather_than_growing_without_bound() {
let mut log = WatchLog::default();
for minute in 0..i64::try_from(CAPACITY + 20).expect("fits") {
log.push(at(&log, minute));
}
assert_eq!(log.entries().count(), CAPACITY);
assert_eq!(
log.entries().next().unwrap().text,
format!("thing {}", CAPACITY + 19)
);
}
#[test]
fn no_rule_line_is_offered_when_it_would_sit_at_either_end() {
let mut log = WatchLog::default();
assert_eq!(log.unseen(), None, "nothing says when they were last here");
log.mark_seen();
log.push(at(&log, 10));
assert_eq!(
log.unseen(),
None,
"everything is new; the line would top it"
);
let mut log = WatchLog::default();
log.push(at(&log, 1));
log.push(at(&log, 2));
log.seen_at = Some(log.since.checked_add(Span::new().minutes(5)).unwrap());
assert_eq!(log.unseen(), None, "nothing is new; the line would foot it");
}
#[test]
fn the_line_falls_between_what_arrived_before_and_after() {
let mut log = WatchLog::default();
log.push(at(&log, 1));
log.push(at(&log, 2));
log.seen_at = Some(log.since.checked_add(Span::new().minutes(3)).unwrap());
log.push(at(&log, 4));
log.push(at(&log, 5));
assert_eq!(log.unseen(), Some(2), "the two that arrived after");
}
}