diff options
Diffstat (limited to 'src/filters')
-rw-r--r-- | src/filters/log.rs | 130 | ||||
-rw-r--r-- | src/filters/mod.rs | 229 | ||||
-rw-r--r-- | src/filters/player.rs | 119 |
3 files changed, 478 insertions, 0 deletions
diff --git a/src/filters/log.rs b/src/filters/log.rs new file mode 100644 index 0000000..8d4e0b5 --- /dev/null +++ b/src/filters/log.rs @@ -0,0 +1,130 @@ +//! 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<PartialEvtc, LogResult>; + +#[derive(Debug, Clone)] +struct BossFilter(HashSet<Boss>); + +impl Filter<PartialEvtc, LogResult> 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) + } +} + +/// A `LogFilter` that only accepts logs with one of the given bosses. +pub fn boss(bosses: HashSet<Boss>) -> Box<dyn LogFilter> { + Box::new(BossFilter(bosses)) +} + + +#[derive(Debug, Clone)] +struct OutcomeFilter(HashSet<FightOutcome>); + +impl Filter<PartialEvtc, LogResult> for OutcomeFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.outcome) + } +} + +/// A `LogFilter` that only accepts logs with one of the given outcomes. +/// +/// See also [`success`][success] and [`wipe`][wipe]. +pub fn outcome(outcomes: HashSet<FightOutcome>) -> Box<dyn LogFilter> { + Box::new(OutcomeFilter(outcomes)) +} + +/// A `LogFilter` that only accepts successful logs. +/// +/// See also [`outcome`][outcome] and [`wipe`][wipe]. +pub fn success() -> Box<dyn LogFilter> { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Success); + outcome(outcomes) +} + +/// A `LogFilter` that only accepts failed logs. +/// +/// See also [`outcome`][outcome] and [`success`][wipe]. +pub fn wipe() -> Box<dyn LogFilter> { + let mut outcomes = HashSet::new(); + outcomes.insert(FightOutcome::Wipe); + outcome(outcomes) +} + +#[derive(Debug, Clone)] +struct WeekdayFilter(HashSet<Weekday>); + +impl Filter<PartialEvtc, LogResult> for WeekdayFilter { + fn filter(&self, log: &LogResult) -> bool { + self.0.contains(&log.time.weekday()) + } +} + +/// A `LogFilter` that only accepts logs if they were done on one of the given weekdays. +pub fn weekday(weekdays: HashSet<Weekday>) -> Box<dyn LogFilter> { + Box::new(WeekdayFilter(weekdays)) +} + + +#[derive(Debug, Clone)] +struct TimeFilter(Option<NaiveDateTime>, Option<NaiveDateTime>); + +impl Filter<PartialEvtc, LogResult> 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 + } +} + +/// A `LogFilter` that only accepts logs in the given time frame. +/// +/// If a bound is not given, -Infinity is assumed for the lower bound, and Infinity for the upper +/// bound. +pub fn time(lower: Option<NaiveDateTime>, upper: Option<NaiveDateTime>) -> Box<dyn LogFilter> { + Box::new(TimeFilter(lower, upper)) +} + +/// A `LogFilter` that only accepts logs after the given date. +/// +/// Also see [`time`][time] and [`before`][before]. +pub fn after(when: NaiveDateTime) -> Box<dyn LogFilter> { + time(Some(when), None) +} + +/// A `LogFilter` that only accepts logs before the given date. +/// +/// Also see [`time`][time] and [`after`][after]. +pub fn before(when: NaiveDateTime) -> Box<dyn LogFilter> { + time(None, Some(when)) +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs new file mode 100644 index 0000000..162b6f8 --- /dev/null +++ b/src/filters/mod.rs @@ -0,0 +1,229 @@ +use std::{fmt, 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<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<Early, Late>: Send + Sync + fmt::Debug { + /// 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)] +struct Const(pub bool); + +impl<E, L> Filter<E, L> for Const { + fn filter_early(&self, _: &E) -> Inclusion { + self.0.into() + } + + fn filter(&self, _: &L) -> bool { + self.0 + } +} + +/// Construct a `Filter` that always returns a fixed value. +pub fn constant<E, L>(output: bool) -> Box<dyn Filter<E, L>> { + Box::new(Const(output)) +} + +struct AndFilter<E, L>(Box<dyn Filter<E, L>>, Box<dyn Filter<E, L>>); + +impl<E, L> Filter<E, L> for AndFilter<E, L> { + 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<E: 'static, L: 'static> ops::BitAnd<Box<dyn Filter<E, L>>> for Box<dyn Filter<E, L>> { + type Output = Box<dyn Filter<E, L>>; + + fn bitand(self, rhs: Box<dyn Filter<E, L>>) -> Self::Output { + Box::new(AndFilter(self, rhs)) + } +} + +impl<E, L> fmt::Debug for AndFilter<E, L> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({:?}) and ({:?})", self.0, self.1) + } +} + +struct OrFilter<E, L>(Box<dyn Filter<E, L>>, Box<dyn Filter<E, L>>); + +impl<E, L> Filter<E, L> for OrFilter<E, L> { + 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<E: 'static, L: 'static> ops::BitOr<Box<dyn Filter<E, L>>> for Box<dyn Filter<E, L>> { + type Output = Box<dyn Filter<E, L>>; + + fn bitor(self, rhs: Box<dyn Filter<E, L>>) -> Self::Output { + Box::new(OrFilter(self, rhs)) + } +} + +impl<E, L> fmt::Debug for OrFilter<E, L> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "({:?}) or ({:?})", self.0, self.1) + } +} + +struct NotFilter<E, L>(Box<dyn Filter<E, L>>); + +impl<E, L> Filter<E, L> for NotFilter<E, L> { + 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<E: 'static, L: 'static> ops::Not for Box<dyn Filter<E, L>> { + type Output = Box<dyn Filter<E, L>>; + + fn not(self) -> Self::Output { + Box::new(NotFilter(self)) + } +} + +impl<E, L> fmt::Debug for NotFilter<E, L> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "not ({:?})", self.0) + } +} + +#[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..4daeb22 --- /dev/null +++ b/src/filters/player.rs @@ -0,0 +1,119 @@ +//! 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<Agent, Player>; + +/// 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. +#[derive(Debug)] +struct AllPlayers(Box<dyn PlayerFilter>); + +impl Filter<PartialEvtc, LogResult> 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<dyn PlayerFilter>) -> Box<dyn LogFilter> { + 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<dyn PlayerFilter>) -> Box<dyn LogFilter> { + !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)] +struct NameFilter(SearchField, Regex); + +impl Filter<Agent, Player> 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), + } + } +} + +/// Construct a `PlayerFilter` that searches the given `field` with the given `regex`. +pub fn name(field: SearchField, regex: Regex) -> Box<dyn PlayerFilter> { + Box::new(NameFilter(field, regex)) +} + +/// Construct a `PlayerFilter` that searches the character name with the given `regex`. +pub fn character(regex: Regex) -> Box<dyn PlayerFilter> { + name(SearchField::Character, regex) +} + +/// Construct a `PlayerFilter` that searches the account name with the given `regex`. +pub fn account(regex: Regex) -> Box<dyn PlayerFilter> { + name(SearchField::Account, regex) +} |