use std::collections::HashMap;
use crate::Position;
use crate::query::Query;
const DEFAULT_MAX_ENTRIES: usize = 1 << 20;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Verdict {
DefinitelyNoMatch,
Unknown,
}
#[derive(Debug)]
pub struct TagTips {
max_pos: HashMap<Box<str>, Position>,
window_floor: Position,
window: u64,
max_entries: usize,
}
impl TagTips {
pub fn new(window_floor: Position, window: u64) -> Self {
TagTips {
max_pos: HashMap::new(),
window_floor,
window,
max_entries: DEFAULT_MAX_ENTRIES,
}
}
#[cfg(test)]
fn record(&mut self, tag: &str, position: Position) {
self.max_pos
.entry(Box::from(tag))
.and_modify(|p| {
if position > *p {
*p = position;
}
})
.or_insert(position);
}
fn max_for(&self, tag: &str) -> Position {
self.max_pos.get(tag).copied().unwrap_or(self.window_floor)
}
#[cfg(test)]
fn window_floor(&self) -> Position {
self.window_floor
}
pub fn may_match(&self, query: &Query, after: Position) -> Verdict {
match query {
Query::All => Verdict::Unknown,
Query::Items(items) => {
for item in items {
if self.item_unknown(item, after) {
return Verdict::Unknown;
}
}
Verdict::DefinitelyNoMatch
}
}
}
fn item_unknown(&self, item: &crate::query::QueryItem, after: Position) -> bool {
if item.tags.is_empty() {
return true;
}
item.tags.iter().all(|t| self.max_for(t.as_str()) > after)
}
pub fn absorb(&mut self, staged: StagedTips, next_position: Position) {
for (tag, position) in staged.tag_positions {
self.max_pos
.entry(tag)
.and_modify(|p| {
if position > *p {
*p = position;
}
})
.or_insert(position);
}
self.evict_if_needed(next_position);
}
fn evict_if_needed(&mut self, next_position: Position) {
if self.max_pos.len() <= self.max_entries {
return;
}
let target = Position::new(next_position.get().saturating_sub(self.window));
let new_floor = self.window_floor.max(target);
self.window_floor = new_floor;
self.max_pos.retain(|_, &mut pos| pos > new_floor);
}
#[cfg(test)]
fn with_max_entries(window_floor: Position, window: u64, max_entries: usize) -> Self {
TagTips {
max_pos: HashMap::new(),
window_floor,
window,
max_entries,
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.max_pos.len()
}
}
#[derive(Debug, Default)]
pub struct StagedTips {
tag_positions: HashMap<Box<str>, Position>,
}
impl StagedTips {
pub fn new() -> Self {
StagedTips::default()
}
pub fn record(&mut self, tag: &str, position: Position) {
self.tag_positions
.entry(Box::from(tag))
.and_modify(|p| {
if position > *p {
*p = position;
}
})
.or_insert(position);
}
pub fn is_empty(&self) -> bool {
self.tag_positions.is_empty()
}
fn contains(&self, tag: &str) -> bool {
self.tag_positions.contains_key(tag)
}
pub fn may_conflict(&self, query: &Query) -> bool {
if self.is_empty() {
return false;
}
match query {
Query::All => true,
Query::Items(items) => items
.iter()
.any(|item| item.tags.iter().all(|t| self.contains(t.as_str()))),
}
}
}
#[cfg(test)]
mod tests {
use tephra_types::{QueryItem, Tag};
use super::*;
fn ty(s: &str) -> crate::event::EventType {
crate::event::EventType::new(s).unwrap()
}
fn tags(items: &[&str]) -> crate::event::Tags {
use smallvec::SmallVec;
crate::event::Tags::new(
items
.iter()
.map(|s| Tag::new(*s).unwrap())
.collect::<SmallVec<[crate::event::Tag; 4]>>(),
)
.unwrap()
}
fn pos(n: u64) -> Position {
Position::new(n)
}
#[test]
fn cold_map_is_all_unknown() {
let tips = TagTips::new(pos(100), 1_000);
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert_eq!(tips.may_match(&q, pos(50)), Verdict::Unknown);
assert_eq!(tips.may_match(&q, pos(99)), Verdict::Unknown);
}
#[test]
fn recorded_tag_after_the_bound_is_unknown() {
let mut tips = TagTips::new(pos(1), 1_000);
tips.record("course:c1", pos(10));
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert_eq!(tips.may_match(&q, pos(5)), Verdict::Unknown);
}
#[test]
fn recorded_tag_at_or_before_the_bound_is_no_match() {
let mut tips = TagTips::new(pos(1), 1_000);
tips.record("course:c1", pos(10));
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert_eq!(tips.may_match(&q, pos(10)), Verdict::DefinitelyNoMatch);
assert_eq!(tips.may_match(&q, pos(11)), Verdict::DefinitelyNoMatch);
}
#[test]
fn absent_tag_ruled_out_only_at_or_above_floor() {
let tips = TagTips::new(pos(100), 1_000);
let q = Query::item(QueryItem::with_tags(tags(&["ghost:x"])));
assert_eq!(tips.may_match(&q, pos(100)), Verdict::DefinitelyNoMatch);
assert_eq!(tips.may_match(&q, pos(150)), Verdict::DefinitelyNoMatch);
assert_eq!(tips.may_match(&q, pos(99)), Verdict::Unknown);
}
#[test]
fn and_within_item_rejects_if_any_tag_ruled_out() {
let mut tips = TagTips::new(pos(1), 1_000);
tips.record("course:c1", pos(10)); let q = Query::item(QueryItem::with_tags(tags(&["course:c1", "student:s1"])));
assert_eq!(tips.may_match(&q, pos(5)), Verdict::DefinitelyNoMatch);
}
#[test]
fn or_across_items_is_unknown_if_any_item_unknown() {
let mut tips = TagTips::new(pos(1), 1_000);
tips.record("course:c1", pos(10));
let q = Query::items(vec![
QueryItem::with_tags(tags(&["student:s1"])),
QueryItem::with_tags(tags(&["course:c1"])),
]);
assert_eq!(tips.may_match(&q, pos(5)), Verdict::Unknown);
}
#[test]
fn empty_tags_item_is_always_unknown() {
let tips = TagTips::new(pos(1), 1_000);
let q = Query::item(QueryItem::of_types(vec![ty("Registered")]));
assert_eq!(tips.may_match(&q, pos(5)), Verdict::Unknown);
}
#[test]
fn all_query_is_unknown() {
let tips = TagTips::new(pos(1), 1_000);
assert_eq!(tips.may_match(&Query::all(), pos(5)), Verdict::Unknown);
}
#[test]
fn eviction_raises_floor_and_drops_old_entries() {
let mut tips = TagTips::with_max_entries(pos(1), 10, 2);
tips.record("a", pos(100));
tips.record("b", pos(101));
tips.record("c", pos(102)); let staged = StagedTips::new();
tips.absorb(staged, pos(103)); assert_eq!(tips.window_floor(), pos(93));
assert_eq!(tips.len(), 3);
}
#[test]
fn floor_never_lowers() {
let mut tips = TagTips::with_max_entries(pos(500), 10, 0);
tips.record("a", pos(600));
tips.absorb(StagedTips::new(), pos(100));
assert_eq!(tips.window_floor(), pos(500));
}
#[test]
fn empty_staged_never_conflicts() {
let staged = StagedTips::new();
let q = Query::item(QueryItem::with_tags(tags(&["course:c1"])));
assert!(!staged.may_conflict(&q));
assert!(!staged.may_conflict(&Query::all()));
}
#[test]
fn staged_conflicts_on_full_tag_set() {
let mut staged = StagedTips::new();
staged.record("course:c1", pos(10));
staged.record("student:s1", pos(10));
let q = Query::item(QueryItem::with_tags(tags(&["course:c1", "student:s1"])));
assert!(staged.may_conflict(&q));
}
#[test]
fn staged_no_conflict_on_partial_tag_set() {
let mut staged = StagedTips::new();
staged.record("course:c1", pos(10));
let q = Query::item(QueryItem::with_tags(tags(&["course:c1", "student:s1"])));
assert!(!staged.may_conflict(&q));
}
#[test]
fn staged_conflicts_ignore_type() {
let mut staged = StagedTips::new();
staged.record("course:c1", pos(10));
let q = Query::item(QueryItem::new(vec![ty("SomeType")], tags(&["course:c1"])));
assert!(staged.may_conflict(&q));
}
#[test]
fn staged_all_query_conflicts_when_non_empty() {
let mut staged = StagedTips::new();
staged.record("x:1", pos(10));
assert!(staged.may_conflict(&Query::all()));
}
}