aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/fexpr/grammar.lalrpop37
-rw-r--r--src/fexpr/mod.rs3
-rw-r--r--src/filters/log.rs2
-rw-r--r--src/filters/mod.rs1
-rw-r--r--src/filters/values.rs144
-rw-r--r--src/main.rs12
6 files changed, 195 insertions, 4 deletions
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<dyn filters::log::LogFilter> = {
"any" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
"exists" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
+ <Comparison<DateProducer>>,
+ <Comparison<DurationProducer>>,
+
"(" <LogFilter> ")",
}
@@ -175,6 +181,22 @@ Date: DateTime<Utc> = {
.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<T>: Box<dyn filters::log::LogFilter> = {
+ <lhs:T> <op:CompOp> <rhs:T> => filters::values::comparison(lhs, op, rhs),
+}
+
Comma<T>: HashSet<T> = {
<v:(<T> ",")*> <e:T> => {
let mut result = v.into_iter().collect::<HashSet<_>>();
@@ -183,6 +205,16 @@ Comma<T>: HashSet<T> = {
},
}
+DateProducer: Box<dyn DateProducer> = {
+ <Date> => filters::values::constant(<>),
+ "-time" => filters::values::time(),
+}
+
+DurationProducer: Box<dyn DurationProducer> = {
+ <Duration> => 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<Output = chrono::DateTime<chrono::Utc>>;
+trait DurationProducer = filters::values::Producer<Output = chrono::Duration>;
+
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<DateTime<Utc>> {
+pub(crate) 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")
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<Self::Output> {
+ 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<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(&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<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()
+ }
+}
+
+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(super::log::datetime_from_filename)
+ }
+
+ fn produce(&self, log: &LogResult) -> Self::Output {
+ log.time
+ }
+}
+
+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
+ }
+}
+
+pub fn duration() -> Box<dyn Producer<Output = Duration>> {
+ 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<Utc>,
+ /// The duration of the fight.
+ duration: Duration,
/// The boss.
boss: Option<Boss>,
/// A vector of all participating players.
@@ -550,9 +552,17 @@ fn extract_info(path: &Path, log: &Log) -> LogResult {
.collect::<Vec<Player>>();
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),