aboutsummaryrefslogtreecommitdiff
path: root/src/filters
diff options
context:
space:
mode:
Diffstat (limited to 'src/filters')
-rw-r--r--src/filters/log.rs70
-rw-r--r--src/filters/mod.rs5
-rw-r--r--src/filters/values.rs281
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));
+ }
+}