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 /src | |
| parent | 1fb3d3259d23410f8bf9879f64de880a11e4f876 (diff) | |
| parent | e22b79adaaa69761fc520d6cd57baee1025fa926 (diff) | |
| download | raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.gz raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.tar.bz2 raidgrep-86e0f74bd7f0301886c1dda0f147b6c8ffbdd707.zip | |
Merge branch 'comparison-filters'
Diffstat (limited to 'src')
| -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 | 
9 files changed, 379 insertions, 72 deletions
| 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), | 
