//! 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 = 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 { 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( Box>, CompOp, Box>, ); impl Debug for Comparator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({:?} {} {:?})", self.0, self.1, self.2) } } impl Filter for Comparator 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( lhs: Box>, op: CompOp, rhs: Box>, ) -> Box where V: Ord, { Box::new(Comparator(lhs, op, rhs)) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct ConstantProducer(V); impl Producer for ConstantProducer { type Output = V; fn produce_early(&self, _: &EarlyLogResult) -> Option { 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( value: V, ) -> Box> { Box::new(ConstantProducer(value)) } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] struct TimeProducer; impl Producer for TimeProducer { type Output = DateTime; fn produce_early(&self, early_log: &EarlyLogResult) -> Option { 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> { 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>> { 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> { Box::new(DurationProducer) } #[derive(Debug)] struct PlayerCountProducer(Box); impl Producer for PlayerCountProducer { type Output = u8; fn produce_early(&self, early_log: &EarlyLogResult) -> Option { 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) -> Box> { 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)); } }