#![allow(clippy::new_ret_no_self)] use std::collections::HashSet; use std::ops; use evtclib::raw::parser::PartialEvtc; use evtclib::statistics::gamedata::Boss; use evtclib::{Agent, AgentName}; use num_derive::FromPrimitive; use num_traits::FromPrimitive as _; use regex::Regex; use super::{guilds, FightOutcome, LogResult, SearchField, Weekday}; use chrono::{Datelike, NaiveDateTime}; /// Early filtering result. /// /// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic), /// similar to SQL's `NULL`. #[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)] #[repr(i8)] pub enum Inclusion { /// The log should be included. Include = 1, /// The state is yet unknown. Unknown = 0, /// The log should be excluded. Exclude = -1, } impl ops::Not for Inclusion { type Output = Self; fn not(self) -> Self::Output { Inclusion::from_i8(-(self as i8)).unwrap() } } impl ops::BitAnd for Inclusion { type Output = Self; fn bitand(self, rhs: Inclusion) -> Self::Output { Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap() } } impl ops::BitOr for Inclusion { type Output = Self; fn bitor(self, rhs: Inclusion) -> Self::Output { Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap() } } impl From for Inclusion { fn from(data: bool) -> Self { if data { Inclusion::Include } else { Inclusion::Exclude } } } /// The main filter trait. /// /// Filters are usually handled as a `Box`. pub trait Filter: Send + Sync { /// Determine early (before processing all events) whether the log stands a chance to be /// included. /// /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite /// answer. fn filter_early(&self, _: &PartialEvtc) -> Inclusion { Inclusion::Unknown } /// Return whether the log should be included, according to this filter. fn filter(&self, log: &LogResult) -> bool; } #[derive(Debug, Clone, Copy)] pub struct Const(pub bool); impl Const { pub fn new(output: bool) -> Box { Box::new(Const(output)) } } impl Filter for Const { fn filter_early(&self, _: &PartialEvtc) -> Inclusion { self.0.into() } fn filter(&self, _: &LogResult) -> bool { self.0 } } struct AndFilter(Box, Box); impl Filter for AndFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { let lhs = self.0.filter_early(partial_evtc); // Short circuit behaviour if lhs == Inclusion::Exclude { Inclusion::Exclude } else { lhs & self.1.filter_early(partial_evtc) } } fn filter(&self, log: &LogResult) -> bool { self.0.filter(log) && self.1.filter(log) } } impl ops::BitAnd> for Box { type Output = Box; fn bitand(self, rhs: Box) -> Self::Output { Box::new(AndFilter(self, rhs)) } } struct OrFilter(Box, Box); impl Filter for OrFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { let lhs = self.0.filter_early(partial_evtc); // Short circuit behaviour if lhs == Inclusion::Include { Inclusion::Include } else { lhs | self.1.filter_early(partial_evtc) } } fn filter(&self, log: &LogResult) -> bool { self.0.filter(log) || self.1.filter(log) } } impl ops::BitOr> for Box { type Output = Box; fn bitor(self, rhs: Box) -> Self::Output { Box::new(OrFilter(self, rhs)) } } struct NotFilter(Box); impl Filter for NotFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { !self.0.filter_early(partial_evtc) } fn filter(&self, log: &LogResult) -> bool { !self.0.filter(log) } } impl ops::Not for Box { type Output = Box; fn not(self) -> Self::Output { Box::new(NotFilter(self)) } } // From here onwards, we have the specific filters /// Filter that filters according to the name. /// /// The given SearchField determines in which field something should be searched. #[derive(Debug, Clone)] pub struct NameFilter(SearchField, Regex); impl NameFilter { pub fn new(field: SearchField, regex: Regex) -> Box { Box::new(NameFilter(field, regex)) } } impl Filter for NameFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { if self.0 == SearchField::Guild { return Inclusion::Unknown; } for player in &partial_evtc.agents { let fancy = Agent::from_raw(player); if let Ok(AgentName::Player { ref account_name, ref character_name, .. }) = fancy.as_ref().map(Agent::name) { let field = match self.0 { SearchField::Account => account_name, SearchField::Character => character_name, _ => unreachable!("We already checked for Guild earlier"), }; if self.1.is_match(field) { return Inclusion::Include; } } } Inclusion::Exclude } fn filter(&self, log: &LogResult) -> bool { for player in &log.players { match self.0 { SearchField::Account if self.1.is_match(&player.account_name) => return true, SearchField::Character if self.1.is_match(&player.character_name) => return true, SearchField::Guild => { let guild_ok = player .guild_id .as_ref() .and_then(|id| guilds::lookup(id)) .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name())) .unwrap_or(false); if guild_ok { return true; } } _ => (), } } false } } #[derive(Debug, Clone)] pub struct BossFilter(HashSet); impl BossFilter { pub fn new(bosses: HashSet) -> Box { Box::new(BossFilter(bosses)) } } impl Filter for BossFilter { fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { let boss = Boss::from_u16(partial_evtc.header.combat_id); boss.map(|b| self.0.contains(&b).into()) .unwrap_or(Inclusion::Include) } fn filter(&self, log: &LogResult) -> bool { let boss = Boss::from_u16(log.boss_id); boss.map(|b| self.0.contains(&b)).unwrap_or(false) } } #[derive(Debug, Clone)] pub struct OutcomeFilter(HashSet); impl OutcomeFilter { pub fn new(outcomes: HashSet) -> Box { Box::new(OutcomeFilter(outcomes)) } } impl Filter for OutcomeFilter { fn filter(&self, log: &LogResult) -> bool { self.0.contains(&log.outcome) } } #[derive(Debug, Clone)] pub struct WeekdayFilter(HashSet); impl WeekdayFilter { pub fn new(weekdays: HashSet) -> Box { Box::new(WeekdayFilter(weekdays)) } } impl Filter for WeekdayFilter { fn filter(&self, log: &LogResult) -> bool { self.0.contains(&log.time.weekday()) } } #[derive(Debug, Clone)] pub struct TimeFilter(Option, Option); impl TimeFilter { pub fn new(after: Option, before: Option) -> Box { Box::new(TimeFilter(after, before)) } } impl Filter for TimeFilter { fn filter(&self, log: &LogResult) -> bool { let after_ok = match self.0 { Some(time) => time <= log.time, None => true, }; let before_ok = match self.1 { Some(time) => time >= log.time, None => true, }; after_ok && before_ok } } #[cfg(test)] mod tests { use super::*; #[test] fn test_inclusion_not() { use Inclusion::*; assert_eq!(!Exclude, Include); assert_eq!(!Include, Exclude); assert_eq!(!Unknown, Unknown); } #[test] fn test_inclusion_and() { use Inclusion::*; assert_eq!(Exclude & Exclude, Exclude); assert_eq!(Exclude & Unknown, Exclude); assert_eq!(Exclude & Include, Exclude); assert_eq!(Unknown & Exclude, Exclude); assert_eq!(Unknown & Unknown, Unknown); assert_eq!(Unknown & Include, Unknown); assert_eq!(Include & Exclude, Exclude); assert_eq!(Include & Unknown, Unknown); assert_eq!(Include & Include, Include); } #[test] fn test_inclusion_or() { use Inclusion::*; assert_eq!(Exclude | Exclude, Exclude); assert_eq!(Exclude | Unknown, Unknown); assert_eq!(Exclude | Include, Include); assert_eq!(Unknown | Exclude, Unknown); assert_eq!(Unknown | Unknown, Unknown); assert_eq!(Unknown | Include, Include); assert_eq!(Include | Exclude, Include); assert_eq!(Include | Unknown, Include); assert_eq!(Include | Include, Include); } }