diff options
Diffstat (limited to 'src/filters.rs')
-rw-r--r-- | src/filters.rs | 396 |
1 files changed, 339 insertions, 57 deletions
diff --git a/src/filters.rs b/src/filters.rs index cdd8f36..7da19ec 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,79 +1,361 @@ +#![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_traits::FromPrimitive; - -use super::{guilds, LogResult, Opt, SearchField}; - -use chrono::Datelike; - -/// Do filtering based on the character or account name. -pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool { - for player in &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) - { - if (opt.field.contains(&SearchField::Account) && opt.expression.is_match(account_name)) - || (opt.field.contains(&SearchField::Character) - && opt.expression.is_match(character_name)) +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<Inclusion> 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<Inclusion> 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<bool> 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<dyn Filter>`. +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<dyn Filter> { + 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<dyn Filter>, Box<dyn Filter>); + +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<Box<dyn Filter>> for Box<dyn Filter> { + type Output = Box<dyn Filter>; + + fn bitand(self, rhs: Box<dyn Filter>) -> Self::Output { + Box::new(AndFilter(self, rhs)) + } +} + +struct OrFilter(Box<dyn Filter>, Box<dyn Filter>); + +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<Box<dyn Filter>> for Box<dyn Filter> { + type Output = Box<dyn Filter>; + + fn bitor(self, rhs: Box<dyn Filter>) -> Self::Output { + Box::new(OrFilter(self, rhs)) + } +} + +struct NotFilter(Box<dyn Filter>); + +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<dyn Filter> { + type Output = Box<dyn Filter>; + + 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<dyn Filter> { + 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) { - return true; + 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<Boss>); + +impl BossFilter { + pub fn new(bosses: HashSet<Boss>) -> Box<dyn Filter> { + 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) } - // Don't throw away the log yet if we are searching for guilds - opt.field.contains(&SearchField::Guild) } -/// Do filtering based on the boss ID. -pub fn filter_boss(evtc: &PartialEvtc, opt: &Opt) -> bool { - let boss = Boss::from_u16(evtc.header.combat_id); - boss.map(|b| opt.bosses.contains(&b)).unwrap_or(true) +#[derive(Debug, Clone)] +pub struct OutcomeFilter(HashSet<FightOutcome>); + +impl OutcomeFilter { + pub fn new(outcomes: HashSet<FightOutcome>) -> Box<dyn Filter> { + Box::new(OutcomeFilter(outcomes)) + } } -/// Do filtering based on the fight outcome. -pub fn filter_outcome(result: &LogResult, opt: &Opt) -> bool { - opt.outcome.contains(&result.outcome) +impl Filter for OutcomeFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.outcome) + } } -/// Do filtering based on the weekday of the fight. -pub fn filter_weekday(result: &LogResult, opt: &Opt) -> bool { - opt.weekdays.contains(&result.time.weekday()) +#[derive(Debug, Clone)] +pub struct WeekdayFilter(HashSet<Weekday>); + +impl WeekdayFilter { + pub fn new(weekdays: HashSet<Weekday>) -> Box<dyn Filter> { + Box::new(WeekdayFilter(weekdays)) + } } -/// Do filtering based on encounter time. -pub fn filter_time(result: &LogResult, opt: &Opt) -> bool { - let after_ok = match opt.after { - Some(time) => time <= result.time, - None => true, - }; - let before_ok = match opt.before { - Some(time) => time >= result.time, - None => true, - }; +impl Filter for WeekdayFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.time.weekday()) + } +} - after_ok && before_ok +#[derive(Debug, Clone)] +pub struct TimeFilter(Option<NaiveDateTime>, Option<NaiveDateTime>); + +impl TimeFilter { + pub fn new(after: Option<NaiveDateTime>, before: Option<NaiveDateTime>) -> Box<dyn Filter> { + Box::new(TimeFilter(after, before)) + } } -/// Do filtering based on the guilds. -pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool { - if !opt.guilds { - return true; +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 } - if !opt.field.contains(&SearchField::Guild) { - return true; +} + +#[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); } - result.players.iter().any(|player| { - let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id)); - if let Some(guild) = guild { - opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name()) - } else { - false - } - }) } |