diff options
Diffstat (limited to 'src/filters/values.rs')
-rw-r--r-- | src/filters/values.rs | 281 |
1 files changed, 281 insertions, 0 deletions
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)); + } +} |