aboutsummaryrefslogtreecommitdiff
path: root/src/filters/values.rs
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2020-06-26 16:54:11 +0200
committerDaniel Schadt <kingdread@gmx.de>2020-06-26 16:54:11 +0200
commit86e0f74bd7f0301886c1dda0f147b6c8ffbdd707 (patch)
treedb6dc6aa23b05173c33f9ea5345f7871c8ce62b4 /src/filters/values.rs
parent1fb3d3259d23410f8bf9879f64de880a11e4f876 (diff)
parente22b79adaaa69761fc520d6cd57baee1025fa926 (diff)
downloadraidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.gz
raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.bz2
raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.zip
Merge branch 'comparison-filters'
Diffstat (limited to 'src/filters/values.rs')
-rw-r--r--src/filters/values.rs281
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));
+ }
+}