From 7881ba85ff40f3d22237ef903c7241b56aa9c185 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2020 15:01:10 +0200 Subject: new filter pipeline This is the groundwork for introducing more complex filter queries like `find` has. Filters can be arbitrarily combined with and/or/not and support an "early filter" mode. So far, the filters have been translated pretty mechanically to match the current command line arguments, so now new syntax has been introduced. The NameFilter is not yet in its final version. The goal is to support something like PlayerAll/PlayerExists and have a PlayerFilter that works on single players instead of the complete log, but that might introduce some code duplication as we then need a PlayerFilterAnd, PlayerFilterOr, ... Some digging has to be done into whether we can reduce that duplication without devolving into madness or resorting to macros. Maybe some type-level generic hackery could be done? Maybe an enum instead of dynamic traits should be used, at least for the base functions? --- src/filters.rs | 396 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 339 insertions(+), 57 deletions(-) (limited to 'src/filters.rs') 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 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) { - 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); + +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) } - // 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); + +impl OutcomeFilter { + pub fn new(outcomes: HashSet) -> Box { + 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); + +impl WeekdayFilter { + pub fn new(weekdays: HashSet) -> Box { + 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, Option); + +impl TimeFilter { + pub fn new(after: Option, before: Option) -> Box { + 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 - } - }) } -- cgit v1.2.3