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 From ba491c8a5f6c8c2fa86b12dacf9d80f92da9168a Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 15:18:20 +0200 Subject: split off player filters and log filters As it turns out, we can easily re-use the existing Filter machinery to generalize over LogFilters (which operate on LogResults) and PlayerFilters (which operate on Players). The feature trait_aliases is not strictly needed but makes the function signatures a bit nicer and easier to read, and it reduces the chances of an error (e.g. by using Filter<&PartialEvtc, ...>). --- src/filters.rs | 361 --------------------------------------------------------- 1 file changed, 361 deletions(-) delete mode 100644 src/filters.rs (limited to 'src/filters.rs') diff --git a/src/filters.rs b/src/filters.rs deleted file mode 100644 index 7da19ec..0000000 --- a/src/filters.rs +++ /dev/null @@ -1,361 +0,0 @@ -#![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); - } -} -- cgit v1.2.3