From d4a24eef7fd410c147de201d776089e0601317d5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 12 Jun 2020 00:48:18 +0200 Subject: initial work on comparison based filters This enables filters such as -time > 2020-01-01 -time < 2020-02-03 ... for time and duration, and later possibly also for more things (such as a COUNT(...) construct). This work tries to integrate them into the existing filter system as seamless as possible, by providing a Comparator which implements LogFilter. The "type checking" is done at parse time, so nonsensical comparisons like -time > 12s flat out give a parse error. This however might be changed to a more dynamic system with run-time type checking, in which case we could do away with the type parameter on Producer and simply work with a generic Value. The comparator would then return an error if two non-identical types would be compared. Note that the system does not support arithmetic expressions, only simple comparisons to constant values. --- src/fexpr/grammar.lalrpop | 37 +++++++++++- src/fexpr/mod.rs | 3 + src/filters/log.rs | 2 +- src/filters/mod.rs | 1 + src/filters/values.rs | 144 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 12 +++- 6 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/filters/values.rs diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop index 45f4fde..c2df097 100644 --- a/src/fexpr/grammar.lalrpop +++ b/src/fexpr/grammar.lalrpop @@ -4,12 +4,15 @@ use super::{ FightOutcome, filters, PlayerClass, + + DateProducer, + DurationProducer, }; use evtclib::Boss; use std::collections::HashSet; use lalrpop_util::ParseError; -use chrono::{DateTime, Local, TimeZone, Utc, Weekday}; +use chrono::{DateTime, Local, TimeZone, Utc, Weekday, Duration}; use regex::{Regex, RegexBuilder}; grammar; @@ -68,6 +71,9 @@ LogPredicate: Box = { "any" "(" "player" ":" ")" => filters::player::any(<>), "exists" "(" "player" ":" ")" => filters::player::any(<>), + >, + >, + "(" ")", } @@ -175,6 +181,22 @@ Date: DateTime = { .map(|d| d.with_timezone(&Utc)), } +Duration: Duration = { + duration => Duration::seconds(<>[..<>.len() - 1].parse().unwrap()), +} + +CompOp: filters::values::CompOp = { + "<" => filters::values::CompOp::Less, + "<=" => filters::values::CompOp::LessEqual, + "=" => filters::values::CompOp::Equal, + ">=" => filters::values::CompOp::GreaterEqual, + ">" => filters::values::CompOp::Greater, +} + +Comparison: Box = { + => filters::values::comparison(lhs, op, rhs), +} + Comma: HashSet = { ",")*> => { let mut result = v.into_iter().collect::>(); @@ -183,6 +205,16 @@ Comma: HashSet = { }, } +DateProducer: Box = { + => filters::values::constant(<>), + "-time" => filters::values::time(), +} + +DurationProducer: Box = { + => filters::values::constant(<>), + "-duration" => filters::values::duration(), +} + match { "player" => "player", "not" => "not", @@ -194,7 +226,8 @@ match { r"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" => datetime, r"\d\d\d\d-\d\d-\d\d" => date, - r"[\w]+" => word, + r"\d+s" => duration, + r"[[:alpha:]][\w]*" => word, r#""[^"]*""# => string, _ diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs index c6a3a39..1738e44 100644 --- a/src/fexpr/mod.rs +++ b/src/fexpr/mod.rs @@ -11,6 +11,9 @@ use itertools::Itertools; use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError}; use thiserror::Error; +trait DateProducer = filters::values::Producer>; +trait DurationProducer = filters::values::Producer; + lalrpop_mod!(#[allow(clippy::all)] pub grammar, "/fexpr/grammar.rs"); #[derive(Debug)] diff --git a/src/filters/log.rs b/src/filters/log.rs index 8cfdcb4..9ca7d3c 100644 --- a/src/filters/log.rs +++ b/src/filters/log.rs @@ -137,7 +137,7 @@ fn time_is_between( /// /// This expects the filename to have the datetime in the pattern `YYYYmmdd-HHMMSS` somewhere in /// it. -fn datetime_from_filename(name: &OsStr) -> Option> { +pub(crate) 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") diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 162b6f8..e966851 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. /// diff --git a/src/filters/values.rs b/src/filters/values.rs new file mode 100644 index 0000000..543b59c --- /dev/null +++ b/src/filters/values.rs @@ -0,0 +1,144 @@ +use std::{ + cmp::Ordering, + fmt::{self, Debug}, +}; + +use chrono::{DateTime, Duration, Utc}; + +use super::{log::LogFilter, Filter}; +use crate::{EarlyLogResult, LogResult}; + +pub trait Producer: Send + Sync + Debug { + type Output; + + fn produce_early(&self, _early_log: &EarlyLogResult) -> Option { + None + } + + fn produce(&self, log: &LogResult) -> Self::Output; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum CompOp { + Less, + LessEqual, + Equal, + GreaterEqual, + 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 { + 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(&self, log: &LogResult) -> bool { + let lhs = self.0.produce(log); + let rhs = self.2.produce(log); + self.1.matches(lhs.cmp(&rhs)) + } +} + +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() + } +} + +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(super::log::datetime_from_filename) + } + + fn produce(&self, log: &LogResult) -> Self::Output { + log.time + } +} + +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 + } +} + +pub fn duration() -> Box> { + Box::new(DurationProducer) +} diff --git a/src/main.rs b/src/main.rs index ba834ce..9ed67cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::{anyhow, Context, Error, Result}; -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, Duration, TimeZone, Utc}; use colored::Colorize; use log::debug; use regex::Regex; @@ -156,6 +156,8 @@ pub struct LogResult { log_file: PathBuf, /// The time of the recording. time: DateTime, + /// The duration of the fight. + duration: Duration, /// The boss. boss: Option, /// A vector of all participating players. @@ -550,9 +552,17 @@ fn extract_info(path: &Path, log: &Log) -> LogResult { .collect::>(); players.sort(); + let duration = log + .local_end_timestamp() + .and_then(|end| log.local_start_timestamp().map(|start| end - start)) + .map(|x| x as i64) + .map(Duration::seconds) + .unwrap_or_else(Duration::zero); + LogResult { log_file: path.to_path_buf(), time: Utc.timestamp(i64::from(log.local_end_timestamp().unwrap_or(0)), 0), + duration, boss, players, outcome: get_fight_outcome(log), -- cgit v1.2.3