diff options
author | Daniel Schadt <kingdread@gmx.de> | 2020-06-26 16:54:11 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2020-06-26 16:54:11 +0200 |
commit | 86e0f74bd7f0301886c1dda0f147b6c8ffbdd707 (patch) | |
tree | db6dc6aa23b05173c33f9ea5345f7871c8ce62b4 | |
parent | 1fb3d3259d23410f8bf9879f64de880a11e4f876 (diff) | |
parent | e22b79adaaa69761fc520d6cd57baee1025fa926 (diff) | |
download | raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.gz raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.bz2 raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.zip |
Merge branch 'comparison-filters'
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | raidgrep.1.asciidoc | 29 | ||||
-rw-r--r-- | src/fexpr/grammar.lalrpop | 55 | ||||
-rw-r--r-- | src/fexpr/mod.rs | 6 | ||||
-rw-r--r-- | src/filters/log.rs | 70 | ||||
-rw-r--r-- | src/filters/mod.rs | 5 | ||||
-rw-r--r-- | src/filters/values.rs | 281 | ||||
-rw-r--r-- | src/main.rs | 22 | ||||
-rw-r--r-- | src/output/formats.rs | 3 | ||||
-rw-r--r-- | src/output/sorting.rs | 7 | ||||
-rw-r--r-- | src/playerclass.rs | 2 |
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), |