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 -------------------------------------------------- src/filters/log.rs | 95 +++++++++++++ src/filters/mod.rs | 213 +++++++++++++++++++++++++++++ src/filters/player.rs | 109 +++++++++++++++ src/main.rs | 28 ++-- 5 files changed, 433 insertions(+), 373 deletions(-) delete mode 100644 src/filters.rs create mode 100644 src/filters/log.rs create mode 100644 src/filters/mod.rs create mode 100644 src/filters/player.rs (limited to 'src') 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); - } -} diff --git a/src/filters/log.rs b/src/filters/log.rs new file mode 100644 index 0000000..ded4c44 --- /dev/null +++ b/src/filters/log.rs @@ -0,0 +1,95 @@ +//! This module contains specific filters that operate on log files. +//! +//! This is the "base unit", as each file corresponds to one log. Filters on other items (such as +//! players) have to be lifted into log filters first. +use super::{ + super::{FightOutcome, LogResult, Weekday}, + Filter, Inclusion, +}; + +use std::collections::HashSet; + +use evtclib::raw::parser::PartialEvtc; +use evtclib::statistics::gamedata::Boss; + +use chrono::{Datelike, NaiveDateTime}; +use num_traits::FromPrimitive as _; + +/// Filter trait used for filters that operate on complete logs. +pub trait LogFilter = Filter; + +#[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 + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs new file mode 100644 index 0000000..62dc04b --- /dev/null +++ b/src/filters/mod.rs @@ -0,0 +1,213 @@ +#![allow(clippy::new_ret_no_self)] +use std::ops; + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive as _; + +pub mod log; +pub mod player; + +/// 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, _: &Early) -> Inclusion { + Inclusion::Unknown + } + + /// Return whether the log should be included, according to this filter. + fn filter(&self, _: &Late) -> 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, _: &E) -> Inclusion { + self.0.into() + } + + fn filter(&self, _: &L) -> bool { + self.0 + } +} + +struct AndFilter(Box>, Box>); + +impl Filter for AndFilter { + fn filter_early(&self, partial_evtc: &E) -> 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: &L) -> 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: &E) -> 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: &L) -> 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: &E) -> Inclusion { + !self.0.filter_early(partial_evtc) + } + + fn filter(&self, log: &L) -> bool { + !self.0.filter(log) + } +} + +impl ops::Not for Box> { + type Output = Box>; + + fn not(self) -> Self::Output { + Box::new(NotFilter(self)) + } +} + +#[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); + } +} diff --git a/src/filters/player.rs b/src/filters/player.rs new file mode 100644 index 0000000..6cc7713 --- /dev/null +++ b/src/filters/player.rs @@ -0,0 +1,109 @@ +//! This module contains filters pertaining to a single player. +//! +//! Additionally, it provides methods to lift a player filter to a log filter with [`any`][any] and +//! [`all`][all]. +use super::{ + super::{guilds, LogResult, Player, SearchField}, + log::LogFilter, + Filter, Inclusion, +}; + +use evtclib::raw::parser::PartialEvtc; +use evtclib::{Agent, AgentName}; + +use regex::Regex; + +/// Filter type for filters that operate on single players. +pub trait PlayerFilter = Filter; + +/// Struct that lifts a [`PlayerFilter`](traitalias.PlayerFilter.html) to a +/// [`LogFilter`](../log/traitalias.LogFilter.html) by requiring all players to match. +/// +/// This struct will short-circuit once the result is known. +struct AllPlayers(Box); + +impl Filter for AllPlayers { + fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion { + let mut result = Inclusion::Include; + for agent in &partial_evtc.agents { + if !agent.is_player() { + continue; + } + + let agent = Agent::from_raw(agent); + if let Ok(agent) = agent { + result = result & self.0.filter_early(&agent); + } else { + result = result & Inclusion::Unknown; + } + // Short circuit + if result == Inclusion::Exclude { + return result; + } + } + result + } + + fn filter(&self, log: &LogResult) -> bool { + log.players.iter().all(|p| self.0.filter(p)) + } +} + +/// Construct a filter that requires the given `player_filter` to match for all players in a log. +pub fn all(player_filter: Box) -> Box { + Box::new(AllPlayers(player_filter)) +} + +/// Construct a filter that requires the given `player_filter` to match for any player in a log. +pub fn any(player_filter: Box) -> Box { + !all(!player_filter) +} + +/// Filter that filters players according to their 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, agent: &Agent) -> Inclusion { + if self.0 == SearchField::Guild { + return Inclusion::Unknown; + } + + if let AgentName::Player { + ref account_name, + ref character_name, + .. + } = agent.name() + { + let field = match self.0 { + SearchField::Account => account_name, + SearchField::Character => character_name, + _ => unreachable!("We already checked for Guild earlier"), + }; + self.1.is_match(field).into() + } else { + Inclusion::Unknown + } + } + + fn filter(&self, player: &Player) -> bool { + match self.0 { + SearchField::Account => self.1.is_match(&player.account_name), + SearchField::Character => self.1.is_match(&player.character_name), + SearchField::Guild => 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), + } + } +} diff --git a/src/main.rs b/src/main.rs index 41df732..bea03f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#![feature(trait_alias)] use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read, Seek}; @@ -15,7 +16,7 @@ use walkdir::{DirEntry, WalkDir}; use evtclib::{AgentKind, AgentName, EventKind, Log}; mod filters; -use filters::{Filter, Inclusion}; +use filters::{log::LogFilter, Inclusion}; mod guilds; mod logger; mod output; @@ -260,21 +261,24 @@ fn is_log_file(entry: &DirEntry) -> bool { .unwrap_or(false) } -fn build_filter(opt: &Opt) -> Box { - let mut filter = filters::Const::new(false); - for field in opt.field.values() { - filter = filter | filters::NameFilter::new(*field, opt.expression.clone()); - } +fn build_filter(opt: &Opt) -> Box { + let player_filter = opt + .field + .values() + .iter() + .map(|field| filters::player::NameFilter::new(*field, opt.expression.clone())) + .fold(filters::Const::new(false), |a, f| a | f); + let mut filter = filters::player::any(player_filter); if opt.invert { filter = !filter; } filter = filter - & filters::BossFilter::new(opt.bosses.values().clone()) - & filters::OutcomeFilter::new(opt.outcome.values().clone()) - & filters::WeekdayFilter::new(opt.weekdays.values().clone()) - & filters::TimeFilter::new(opt.after, opt.before); + & filters::log::BossFilter::new(opt.bosses.values().clone()) + & filters::log::OutcomeFilter::new(opt.outcome.values().clone()) + & filters::log::WeekdayFilter::new(opt.weekdays.values().clone()) + & filters::log::TimeFilter::new(opt.after, opt.before); filter } @@ -282,7 +286,7 @@ fn build_filter(opt: &Opt) -> Box { /// Run the grep search with the given options. fn grep(opt: &Opt) -> Result<()> { let pipeline = &output::build_pipeline(opt); - let filter: &dyn Filter = &*build_filter(opt); + let filter: &dyn LogFilter = &*build_filter(opt); rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { @@ -309,7 +313,7 @@ fn grep(opt: &Opt) -> Result<()> { /// If the log matches, returns `Ok(Some(..))`. /// If the log doesn't match, returns `Ok(None)`. /// If there was a fatal error, returns `Err(..)`. -fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result> { +fn search_log(entry: &DirEntry, filter: &dyn LogFilter) -> Result> { let file_stream = BufReader::new(File::open(entry.path())?); let is_zip = entry .file_name() -- cgit v1.2.3