aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--raidgrep.1.asciidoc29
-rw-r--r--src/fexpr/grammar.lalrpop55
-rw-r--r--src/fexpr/mod.rs6
-rw-r--r--src/filters/log.rs70
-rw-r--r--src/filters/mod.rs5
-rw-r--r--src/filters/values.rs281
-rw-r--r--src/main.rs22
-rw-r--r--src/output/formats.rs3
-rw-r--r--src/output/sorting.rs7
-rw-r--r--src/playerclass.rs2
11 files changed, 407 insertions, 74 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ed88c4..b2b15a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## Unreleased
### Added
- The `--check` command line argument to check a single file.
+- Comparison based filters (`count`, `-time`, `-duration`).
### Changed
- The exit code will now be dependent on whether logs matching the filter were
diff --git a/raidgrep.1.asciidoc b/raidgrep.1.asciidoc
index fc54f35..3165529 100644
--- a/raidgrep.1.asciidoc
+++ b/raidgrep.1.asciidoc
@@ -144,10 +144,35 @@ Those predicates can be used as-is in the filter:
Include logs in which any player matches the given player predicates. See
below for a list of player predicates.
+=== Comparative Predicates
+
+Some predicates work by comparing a log value against a given constant (or
+other computed value). Comparisons are done using the operators '<', '\<=', '=',
+'>' and '>=' - in the list below, the placeholder '<>' will be used in place
+of one of the comparison operators.
+
+*-time* '<>' 'DATE'::
+ Include logs whose timestamp is in relation to the given date. This is a
+ generalized version of the *-before* and *-after* predicates and accepts
+ the same date formats.
+
+*-duration* '<>' 'DURATION'::
+ Include logs whose duration is in relation to the given duration. The
+ duration is given as minutes and/or seconds: "42m", "42m 42s" or "42s".
+
+*count(player)* '<>' 'COUNT'::
+ Include logs whose player count is in relation to the given count. This
+ counts all players in the log.
+
+*count(player:* 'PREDICATES' *)* '<>' 'COUNT'::
+ Include logs whose player count is in relation to the given count, only
+ counting players that are matching the given player predicates. See below
+ for a list of player predicates.
+
=== Player Predicates
-The following predicates have to be wrapped in either a *any(player: ...)* or
-*all(player: ...)* construct to be accepted.
+The following predicates have to be wrapped in either a *any(player: ...)*,
+*all(player: ...)* or *count(player: ...)* construct to be accepted.
*-character* 'REGEX'::
Matches the player if the character name matches the given regular
diff --git a/src/fexpr/grammar.lalrpop b/src/fexpr/grammar.lalrpop
index 45f4fde..092407e 100644
--- a/src/fexpr/grammar.lalrpop
+++ b/src/fexpr/grammar.lalrpop
@@ -4,12 +4,16 @@ use super::{
FightOutcome,
filters,
PlayerClass,
+
+ DateProducer,
+ DurationProducer,
+ CountProducer,
};
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;
@@ -48,8 +52,12 @@ LogPredicate: Box<dyn filters::log::LogFilter> = {
"-outcome" <Comma<FightOutcome>> => filters::log::outcome(<>),
"-weekday" <Comma<Weekday>> => filters::log::weekday(<>),
- "-before" <Date> => filters::log::before(<>),
- "-after" <Date> => filters::log::after(<>),
+ "-before" <Date> => filters::values::comparison(
+ filters::values::time(), filters::values::CompOp::Less, filters::values::constant(<>)
+ ),
+ "-after" <Date> => filters::values::comparison(
+ filters::values::time(), filters::values::CompOp::Greater, filters::values::constant(<>)
+ ),
"-log-before" <Date> => filters::log::log_before(<>),
"-log-after" <Date> => filters::log::log_after(<>),
@@ -68,6 +76,10 @@ LogPredicate: Box<dyn filters::log::LogFilter> = {
"any" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
"exists" "(" "player" ":" <PlayerFilter> ")" => filters::player::any(<>),
+ <Comparison<DateProducer>>,
+ <Comparison<DurationProducer>>,
+ <Comparison<CountProducer>>,
+
"(" <LogFilter> ")",
}
@@ -175,6 +187,22 @@ Date: DateTime<Utc> = {
.map(|d| d.with_timezone(&Utc)),
}
+Duration: Duration = {
+ duration => Duration::from_std(humantime::parse_duration(<>).unwrap()).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 +211,22 @@ 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(),
+}
+
+CountProducer: Box<dyn CountProducer> = {
+ <integer> => filters::values::constant(<>.parse().unwrap()),
+ "count" "(" "player" ":" <PlayerFilter> ")" => filters::values::player_count(<>),
+ "count" "(" "player" ")" => filters::values::player_count(filters::constant(true)),
+}
+
match {
"player" => "player",
"not" => "not",
@@ -191,10 +235,13 @@ match {
"any" => "any",
"all" => "all",
"exists" => "exists",
+ "count" => "count",
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+m ?)?\d+s)|(\d+m)" => duration,
+ r"\d+" => integer,
+ r"[[:alpha:]][\w]*" => word,
r#""[^"]*""# => string,
_
diff --git a/src/fexpr/mod.rs b/src/fexpr/mod.rs
index c6a3a39..5d12051 100644
--- a/src/fexpr/mod.rs
+++ b/src/fexpr/mod.rs
@@ -11,6 +11,12 @@ use itertools::Itertools;
use lalrpop_util::{lalrpop_mod, lexer::Token, ParseError};
use thiserror::Error;
+// Lalrpop chokes on the associated type specification (it doesn't expect the =), so we need to
+// define those aliases here in Rust and then import and use them in the grammar.
+trait DateProducer = filters::values::Producer<Output = chrono::DateTime<chrono::Utc>>;
+trait DurationProducer = filters::values::Producer<Output = chrono::Duration>;
+trait CountProducer = filters::values::Producer<Output = u8>;
+
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..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));
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index ba834ce..674d128 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;
@@ -17,7 +17,7 @@ use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};
use evtclib::raw::parser::PartialEvtc;
-use evtclib::{Boss, EventKind, Log};
+use evtclib::{Boss, Event, EventKind, Log};
mod fexpr;
mod filters;
@@ -129,10 +129,16 @@ impl Opt {
fn build_filter(&self) -> Result<Box<dyn LogFilter>> {
// As a shortcut, we allow only the regular expression to be given, to retain the behaviour
// before the filter changes.
+
+ // Special characters that when present will prevent the filter to be interpreted as a
+ // regex. This is to ensure that errors are properly reported on invalid filterlines
+ // instead of being swallowed because the filter was taken as a (valid) regex:
+ const SPECIAL_CHARS: &[char] = &['-', '(', ')', ':', '<', '>', '='];
+
if self.expression.len() == 1 {
let line = &self.expression[0];
let maybe_filter = build_filter(line);
- if maybe_filter.is_err() && !line.starts_with('-') {
+ if maybe_filter.is_err() && !line.contains(SPECIAL_CHARS) {
let maybe_regex = Regex::new(line);
if let Ok(rgx) = maybe_regex {
let filter = filters::player::any(
@@ -156,6 +162,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.
@@ -553,6 +561,7 @@ fn extract_info(path: &Path, log: &Log) -> LogResult {
LogResult {
log_file: path.to_path_buf(),
time: Utc.timestamp(i64::from(log.local_end_timestamp().unwrap_or(0)), 0),
+ duration: get_fight_duration(log),
boss,
players,
outcome: get_fight_outcome(log),
@@ -589,3 +598,10 @@ fn get_fight_outcome(log: &Log) -> FightOutcome {
FightOutcome::Wipe
}
}
+
+/// Get the duration of the fight.
+fn get_fight_duration(log: &Log) -> Duration {
+ let start = log.events().first().map(Event::time).unwrap_or(0) as i64;
+ let end = log.events().last().map(Event::time).unwrap_or(0) as i64;
+ Duration::milliseconds(end - start)
+}
diff --git a/src/output/formats.rs b/src/output/formats.rs
index 51de033..560963b 100644
--- a/src/output/formats.rs
+++ b/src/output/formats.rs
@@ -36,7 +36,7 @@ impl Format for HumanReadable {
};
writeln!(
result,
- "{}: {} - {}: {}{} {}",
+ "{}: {} - {}: {}{} {} after {}",
"Date".green(),
item.time
.with_timezone(&Local)
@@ -47,6 +47,7 @@ impl Format for HumanReadable {
.unwrap_or_else(|| "unknown".into()),
if item.is_cm { " CM" } else { "" },
outcome,
+ humantime::Duration::from(item.duration.to_std().unwrap()),
)
.unwrap();
for player in &item.players {
diff --git a/src/output/sorting.rs b/src/output/sorting.rs
index f46a95c..78f3538 100644
--- a/src/output/sorting.rs
+++ b/src/output/sorting.rs
@@ -130,6 +130,7 @@ mod tests {
use super::*;
use chrono::prelude::*;
+ use chrono::Duration;
use evtclib::Boss as B;
#[test]
@@ -163,10 +164,12 @@ mod tests {
fn test_sorting_cmp() {
use Component::*;
+ let duration = Duration::zero();
let logs: &[&LogResult] = &[
&LogResult {
log_file: "".into(),
time: Utc.ymd(2020, 4, 3).and_hms(12, 0, 0),
+ duration,
boss: Some(B::Dhuum),
players: vec![],
outcome: FightOutcome::Success,
@@ -175,6 +178,7 @@ mod tests {
&LogResult {
log_file: "".into(),
time: Utc.ymd(2020, 4, 3).and_hms(13, 0, 0),
+ duration,
boss: Some(B::Dhuum),
players: vec![],
outcome: FightOutcome::Success,
@@ -183,6 +187,7 @@ mod tests {
&LogResult {
log_file: "".into(),
time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
+ duration,
boss: Some(B::Dhuum),
players: vec![],
outcome: FightOutcome::Success,
@@ -191,6 +196,7 @@ mod tests {
&LogResult {
log_file: "".into(),
time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
+ duration,
boss: Some(B::Qadim),
players: vec![],
outcome: FightOutcome::Success,
@@ -199,6 +205,7 @@ mod tests {
&LogResult {
log_file: "".into(),
time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0),
+ duration,
boss: Some(B::Dhuum),
players: vec![],
outcome: FightOutcome::Success,
diff --git a/src/playerclass.rs b/src/playerclass.rs
index 31a49aa..ba207e6 100644
--- a/src/playerclass.rs
+++ b/src/playerclass.rs
@@ -14,7 +14,7 @@ use thiserror::Error;
/// probably don't want any Dragonhunters.
///
/// So this enum unifies the handling between core specs and elite specs, and provides them with a
-/// convenient [`Display`][Display] implementation as well.
+/// convenient [`Display`][fmt::Display] implementation as well.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PlayerClass {
Profession(Profession),