diff options
Diffstat (limited to 'src/filters')
| -rw-r--r-- | src/filters/log.rs | 70 | ||||
| -rw-r--r-- | src/filters/mod.rs | 5 | ||||
| -rw-r--r-- | src/filters/values.rs | 281 | 
3 files changed, 293 insertions, 63 deletions
diff --git a/src/filters/log.rs b/src/filters/log.rs index 8cfdcb4..10258a0 100644 --- a/src/filters/log.rs +++ b/src/filters/log.rs @@ -7,17 +7,12 @@ use super::{      Filter, Inclusion,  }; -use std::{collections::HashSet, ffi::OsStr}; +use std::collections::HashSet;  use evtclib::Boss; -use chrono::{DateTime, Datelike, Local, TimeZone, Utc, Weekday}; +use chrono::{DateTime, Datelike, Utc, Weekday};  use num_traits::FromPrimitive as _; -use once_cell::sync::Lazy; -use regex::Regex; - -/// The regular expression used to extract datetimes from filenames. -static DATE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\d{8}-\d{6}").unwrap());  /// Filter trait used for filters that operate on complete logs.  pub trait LogFilter = Filter<EarlyLogResult, LogResult>; @@ -91,23 +86,9 @@ pub fn weekday(weekdays: HashSet<Weekday>) -> Box<dyn LogFilter> {  }  #[derive(Debug, Clone)] -struct TimeFilter(Option<DateTime<Utc>>, Option<DateTime<Utc>>, bool); +struct TimeFilter(Option<DateTime<Utc>>, Option<DateTime<Utc>>);  impl Filter<EarlyLogResult, LogResult> for TimeFilter { -    fn filter_early(&self, early_log: &EarlyLogResult) -> Inclusion { -        // Ignore the filename heuristic if the user wishes so. -        if !self.2 { -            return Inclusion::Unknown; -        } -        early_log -            .log_file -            .file_name() -            .and_then(datetime_from_filename) -            .map(|d| time_is_between(d, self.0, self.1)) -            .map(Into::into) -            .unwrap_or(Inclusion::Unknown) -    } -      fn filter(&self, log: &LogResult) -> bool {          time_is_between(log.time, self.0, self.1)      } @@ -133,54 +114,20 @@ fn time_is_between(      after_ok && before_ok  } -/// Try to extract the log time from the filename. -/// -/// This expects the filename to have the datetime in the pattern `YYYYmmdd-HHMMSS` somewhere in -/// it. -fn datetime_from_filename(name: &OsStr) -> Option<DateTime<Utc>> { -    let date_match = DATE_REGEX.find(name.to_str()?)?; -    let local_time = Local -        .datetime_from_str(date_match.as_str(), "%Y%m%d-%H%M%S") -        .ok()?; -    Some(local_time.with_timezone(&Utc)) -} - -/// 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<DateTime<Utc>>, upper: Option<DateTime<Utc>>) -> Box<dyn LogFilter> { -    Box::new(TimeFilter(lower, upper, true)) -} - -/// A `LogFilter` that only accepts logs after the given date. -/// -/// Also see [`time`][time] and [`before`][before]. -pub fn after(when: DateTime<Utc>) -> 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: DateTime<Utc>) -> Box<dyn LogFilter> { -    time(None, Some(when)) -} -  /// A `LogFilter` that only accepts logs in the given time frame.  /// -/// Compared to [`time`][time], this filter ignores the file name. This can result in more accurate -/// results if you renamed logs, but if also leads to a worse runtime. +/// Compared to [`-time`][super::values::time], this filter ignores the file name. This can result +/// in more accurate results if you renamed logs, but if also leads to a worse runtime.  pub fn log_time(lower: Option<DateTime<Utc>>, upper: Option<DateTime<Utc>>) -> Box<dyn LogFilter> { -    Box::new(TimeFilter(lower, upper, false)) +    Box::new(TimeFilter(lower, upper))  } -/// Like [`after`][after], but ignores the file name for date calculations. +/// Only include logs after the given date, but ignore the file name for date calculations.  pub fn log_after(when: DateTime<Utc>) -> Box<dyn LogFilter> {      log_time(Some(when), None)  } -/// Like [`before`][before], but ignores the file name for date calculations. +/// Only include logs before the given date, but ignore the file name for date calculations.  pub fn log_before(when: DateTime<Utc>) -> Box<dyn LogFilter> {      log_time(None, Some(when))  } @@ -202,6 +149,7 @@ pub fn challenge_mote() -> Box<dyn LogFilter> {  #[cfg(test)]  mod tests {      use super::*; +    use chrono::TimeZone;      #[test]      fn test_time_is_between() { diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 162b6f8..7ab0d42 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -5,6 +5,7 @@ use num_traits::FromPrimitive as _;  pub mod log;  pub mod player; +pub mod values;  /// Early filtering result.  /// @@ -62,8 +63,8 @@ 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. +    /// Note that you can return [Inclusion::Unknown] if this filter cannot determine yet a +    /// definite answer.      fn filter_early(&self, _: &Early) -> Inclusion {          Inclusion::Unknown      } diff --git a/src/filters/values.rs b/src/filters/values.rs new file mode 100644 index 0000000..a523dad --- /dev/null +++ b/src/filters/values.rs @@ -0,0 +1,281 @@ +//! Value extractor system for raidgrep filters. +//! +//! [`Comparators`][Comparator] are special filters that work by first producing a value from a +//! given log (via the [`Producer`][Producer]) trait and then applying a comparison operator +//! ([`CompOp`][CompOp]) between the results. This type of filter gives a lot of flexibility, as it +//! can reduce the number of hard-coded filters one has to create (for example, `-before` and +//! `-after` can be merged). +//! +//! A [`Comparator`][Comparator] can only compare producers which produce the same type of value, +//! and that value must define a total order (i.e. it must implement [`Ord`][Ord]). Note that the +//! actual comparison is done on filter time, that is a [`Comparator`][Comparator] is basically a +//! filter that first uses the producers to produce a value from the given log, and then compares +//! the two resulting values with the given comparison operator. +use std::{ +    cmp::Ordering, +    convert::TryFrom, +    ffi::OsStr, +    fmt::{self, Debug}, +}; + +use chrono::{DateTime, Duration, Local, TimeZone, Utc}; +use evtclib::Agent; +use once_cell::sync::Lazy; +use regex::Regex; + +use super::{log::LogFilter, player::PlayerFilter, Filter, Inclusion}; +use crate::{EarlyLogResult, LogResult}; + +/// The regular expression used to extract datetimes from filenames. +static DATE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\d{8}-\d{6}").unwrap()); + +/// A producer for a given value. +/// +/// A producer is something that produces a value of a certain type from a log, which can then be +/// used by [`Comparators`][Comparator] to do the actual comparison. +pub trait Producer: Send + Sync + Debug { +    /// Type of the value that will be produced. +    type Output; + +    /// Early production. +    /// +    /// This function should be implemented if the value can already be produced without the +    /// complete log being parsed. This can speed up filtering if a lot of logs end up being thrown +    /// away. +    /// +    /// If a value cannot be produced early, `None` should be returned. +    fn produce_early(&self, _early_log: &EarlyLogResult) -> Option<Self::Output> { +        None +    } + +    /// Produce the value from the given log. +    fn produce(&self, log: &LogResult) -> Self::Output; +} + +/// The comparison operator to be used. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum CompOp { +    /// The first value must be strictly less than the second value. +    Less, +    /// The first value must be less or equal to the second value. +    LessEqual, +    /// Both values must be equal. +    Equal, +    /// The first value must be greater or equal to the second value. +    GreaterEqual, +    /// The first value must be strictly greater than the second value. +    Greater, +} + +impl fmt::Display for CompOp { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        let symbol = match self { +            CompOp::Less => "<", +            CompOp::LessEqual => "<=", +            CompOp::Equal => "=", +            CompOp::GreaterEqual => ">=", +            CompOp::Greater => ">", +        }; +        f.pad(symbol) +    } +} + +impl CompOp { +    /// Check whether the comparison operator matches the given ordering. +    pub fn matches(self, cmp: Ordering) -> bool { +        match cmp { +            Ordering::Less => self == CompOp::Less || self == CompOp::LessEqual, +            Ordering::Equal => { +                self == CompOp::LessEqual || self == CompOp::Equal || self == CompOp::GreaterEqual +            } +            Ordering::Greater => self == CompOp::Greater || self == CompOp::GreaterEqual, +        } +    } +} + +struct Comparator<V>( +    Box<dyn Producer<Output = V>>, +    CompOp, +    Box<dyn Producer<Output = V>>, +); + +impl<V> Debug for Comparator<V> { +    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +        write!(f, "({:?} {} {:?})", self.0, self.1, self.2) +    } +} + +impl<V> Filter<EarlyLogResult, LogResult> for Comparator<V> +where +    V: Ord, +{ +    fn filter_early(&self, early_log: &EarlyLogResult) -> Inclusion { +        self.0 +            .produce_early(early_log) +            .and_then(|lhs| self.2.produce_early(early_log).map(|rhs| lhs.cmp(&rhs))) +            .map(|ordering| self.1.matches(ordering)) +            .map(Into::into) +            .unwrap_or(Inclusion::Unknown) +    } + +    fn filter(&self, log: &LogResult) -> bool { +        let lhs = self.0.produce(log); +        let rhs = self.2.produce(log); +        self.1.matches(lhs.cmp(&rhs)) +    } +} + +/// Create a log filter that works by comparing two values. +/// +/// The values will be produced by the given producers. +/// +/// This function acts as a "bridge" between the value producers and the log filter system by +/// actually evaluating the comparison. +pub fn comparison<V: 'static>( +    lhs: Box<dyn Producer<Output = V>>, +    op: CompOp, +    rhs: Box<dyn Producer<Output = V>>, +) -> Box<dyn LogFilter> +where +    V: Ord, +{ +    Box::new(Comparator(lhs, op, rhs)) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct ConstantProducer<V>(V); + +impl<V: Send + Sync + Debug + Clone> Producer for ConstantProducer<V> { +    type Output = V; +    fn produce_early(&self, _: &EarlyLogResult) -> Option<Self::Output> { +        Some(self.0.clone()) +    } + +    fn produce(&self, _: &LogResult) -> Self::Output { +        self.0.clone() +    } +} + +/// A producer that always produces the given constant, regardless of the log. +pub fn constant<V: Send + Sync + Debug + Clone + 'static>( +    value: V, +) -> Box<dyn Producer<Output = V>> { +    Box::new(ConstantProducer(value)) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +struct TimeProducer; + +impl Producer for TimeProducer { +    type Output = DateTime<Utc>; + +    fn produce_early(&self, early_log: &EarlyLogResult) -> Option<Self::Output> { +        early_log +            .log_file +            .file_name() +            .and_then(datetime_from_filename) +    } + +    fn produce(&self, log: &LogResult) -> Self::Output { +        log.time +    } +} + +/// Try to extract the log time from the filename. +/// +/// This expects the filename to have the datetime in the pattern `YYYYmmdd-HHMMSS` somewhere in +/// it. +fn datetime_from_filename(name: &OsStr) -> Option<DateTime<Utc>> { +    let date_match = DATE_REGEX.find(name.to_str()?)?; +    let local_time = Local +        .datetime_from_str(date_match.as_str(), "%Y%m%d-%H%M%S") +        .ok()?; +    Some(local_time.with_timezone(&Utc)) +} + +/// A producer that produces the time at which a log was created. +pub fn time() -> Box<dyn Producer<Output = DateTime<Utc>>> { +    Box::new(TimeProducer) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +struct DurationProducer; + +impl Producer for DurationProducer { +    type Output = Duration; + +    fn produce(&self, log: &LogResult) -> Self::Output { +        log.duration +    } +} + +/// A producer that produces the duration that a fight lasted in the log. +pub fn duration() -> Box<dyn Producer<Output = Duration>> { +    Box::new(DurationProducer) +} + +#[derive(Debug)] +struct PlayerCountProducer(Box<dyn PlayerFilter>); + +impl Producer for PlayerCountProducer { +    type Output = u8; + +    fn produce_early(&self, early_log: &EarlyLogResult) -> Option<Self::Output> { +        let mut count = 0; +        for agent in &early_log.evtc.agents { +            if !agent.is_player() { +                continue; +            } + +            let agent = Agent::try_from(agent); +            if let Ok(agent) = agent { +                let result = self.0.filter_early(&agent); +                match result { +                    Inclusion::Include => count += 1, +                    Inclusion::Exclude => (), +                    Inclusion::Unknown => return None, +                } +            } else { +                return None; +            } +        } +        Some(count) +    } + +    fn produce(&self, log: &LogResult) -> Self::Output { +        log.players.iter().filter(|p| self.0.filter(p)).count() as u8 +    } +} + +/// A producer that counts the players matching the given filter. +pub fn player_count(filter: Box<dyn PlayerFilter>) -> Box<dyn Producer<Output = u8>> { +    Box::new(PlayerCountProducer(filter)) +} + +#[cfg(test)] +mod tests { +    use super::*; + +    #[test] +    fn test_compop_matches() { +        assert!(CompOp::Less.matches(Ordering::Less)); +        assert!(!CompOp::Less.matches(Ordering::Equal)); +        assert!(!CompOp::Less.matches(Ordering::Greater)); + +        assert!(CompOp::LessEqual.matches(Ordering::Less)); +        assert!(CompOp::LessEqual.matches(Ordering::Equal)); +        assert!(!CompOp::LessEqual.matches(Ordering::Greater)); + +        assert!(!CompOp::Equal.matches(Ordering::Less)); +        assert!(CompOp::Equal.matches(Ordering::Equal)); +        assert!(!CompOp::Equal.matches(Ordering::Greater)); + +        assert!(!CompOp::GreaterEqual.matches(Ordering::Less)); +        assert!(CompOp::GreaterEqual.matches(Ordering::Equal)); +        assert!(CompOp::GreaterEqual.matches(Ordering::Greater)); + +        assert!(!CompOp::Greater.matches(Ordering::Less)); +        assert!(!CompOp::Greater.matches(Ordering::Equal)); +        assert!(CompOp::Greater.matches(Ordering::Greater)); +    } +}  | 
