diff options
| author | Daniel <kingdread@gmx.de> | 2020-05-14 16:13:28 +0200 | 
|---|---|---|
| committer | Daniel <kingdread@gmx.de> | 2020-05-14 16:13:28 +0200 | 
| commit | c973f2ddb47721254f6f5a81e4c45f0c72d793fd (patch) | |
| tree | 4aa7c00b87eec09ee1faca957be508efe3dbbb65 /src | |
| parent | 331d6b1762d1d9431b210fc98a495d56ad7a1cd1 (diff) | |
| parent | e2d23d4b76000263e9f939637353bbc4bb9289fd (diff) | |
| download | raidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.tar.gz raidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.tar.bz2 raidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.zip | |
Merge branch 'sorted-output'
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 24 | ||||
| -rw-r--r-- | src/output/aggregators.rs | 48 | ||||
| -rw-r--r-- | src/output/mod.rs | 11 | ||||
| -rw-r--r-- | src/output/pipeline.rs | 7 | ||||
| -rw-r--r-- | src/output/sorting.rs | 229 | 
5 files changed, 302 insertions, 17 deletions
| diff --git a/src/main.rs b/src/main.rs index ee1c232..84d1063 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ use filters::{log::LogFilter, Inclusion};  mod guilds;  mod logger;  mod output; +use output::sorting::Sorting;  mod paths;  mod playerclass;  use playerclass::PlayerClass; @@ -79,6 +80,13 @@ pub struct Opt {      #[structopt(long = "no-color")]      no_color: bool, +    /// Sort the output. +    /// +    /// Valid sorting fields are date, boss, cm and outcome. Prefix the field with ~ to sort in +    /// descending order. +    #[structopt(short = "s", long = "sort")] +    sorting: Option<Sorting>, +      /// Print more debugging information to stderr.      #[structopt(long = "debug")]      debug: bool, @@ -102,7 +110,7 @@ pub struct Opt {  }  /// A log that matches the search criteria. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)]  pub struct LogResult {      /// The path to the log file.      log_file: PathBuf, @@ -150,7 +158,7 @@ impl Ord for Player {  }  /// Outcome of the fight. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]  pub enum FightOutcome {      Success,      Wipe, @@ -377,8 +385,9 @@ fn build_filter(expr_string: &str) -> Result<Box<dyn LogFilter>> {  /// Run the grep search with the given options.  fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { -    let pipeline = &output::build_pipeline(opt); -    rayon::scope(|s| { +    let pipeline = output::build_pipeline(opt); +    let pipeline_ref = &pipeline; +    let result: Result<()> = rayon::scope(|s| {          let walker = WalkDir::new(&opt.path);          for entry in walker {              let entry = entry?; @@ -391,7 +400,7 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> {                      let search = search_log(&entry, filter);                      match search {                          Ok(None) => (), -                        Ok(Some(result)) => pipeline.push_item(&result), +                        Ok(Some(result)) => pipeline_ref.push_item(result),                          Err(err) => {                              debug!("Runtime error while scanning {:?}: {}", entry.path(), err);                          } @@ -400,7 +409,10 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> {              });          }          Ok(()) -    }) +    }); +    result?; +    pipeline.finish(); +    Ok(())  }  /// Search the given single log. diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 1b04af3..83171eb 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -6,13 +6,14 @@  //!  //! Aggregators must be shareable across threads, as the search will be multi-threaded. This is why  //! an Aggregator must make sure that the data is protected by a mutex or similar. -use super::{super::LogResult, formats::Format}; +use super::{super::LogResult, formats::Format, sorting::Sorting}; -use std::io::Write; +use std::{io::Write, sync::Mutex};  pub trait Aggregator: Sync { -    fn push_item(&self, item: &LogResult, format: &dyn Format, stream: &mut dyn Write); -    fn finish(self, format: &dyn Format, stream: &mut dyn Write); +    fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write); +    // When the `unsized_locals` feature is stable, we could rewrite this to finish(self, ...). +    fn finish(self: Box<Self>, format: &dyn Format, stream: &mut dyn Write);  }  /// An aggregator that just pushes through each item to the output stream without any sorting or @@ -20,11 +21,44 @@ pub trait Aggregator: Sync {  pub struct WriteThrough;  impl Aggregator for WriteThrough { -    fn push_item(&self, item: &LogResult, format: &dyn Format, stream: &mut dyn Write) { -        let text = format.format_result(item); +    fn push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write) { +        let text = format.format_result(&item);          stream.write_all(text.as_bytes()).unwrap();          stream.flush().unwrap();      } -    fn finish(self, _: &dyn Format, _: &mut dyn Write) {} +    fn finish(self: Box<Self>, _: &dyn Format, _: &mut dyn Write) {} +} + +/// An aggregator that keeps all found logs in memory and sorts them before outputting them. +#[derive(Debug)] +pub struct SortedOutput { +    sorting: Sorting, +    items: Mutex<Vec<LogResult>>, +} + +impl SortedOutput { +    pub fn new(sorting: Sorting) -> Self { +        SortedOutput { +            sorting, +            items: Mutex::new(vec![]), +        } +    } +} + +impl Aggregator for SortedOutput { +    fn push_item(&self, item: LogResult, _: &dyn Format, _: &mut dyn Write) { +        self.items.lock().unwrap().push(item) +    } + +    fn finish(self: Box<Self>, format: &dyn Format, stream: &mut dyn Write) { +        let SortedOutput { sorting, items } = *self; +        let mut items = items.into_inner().unwrap(); +        items.sort_unstable_by(|a, b| sorting.cmp(a, b)); +        for item in items { +            let text = format.format_result(&item); +            stream.write_all(text.as_bytes()).unwrap(); +        } +        stream.flush().unwrap(); +    }  } diff --git a/src/output/mod.rs b/src/output/mod.rs index 0fd92d9..c1cd787 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -5,15 +5,20 @@ use std::io;  pub mod aggregators;  pub mod formats;  pub mod pipeline; +pub mod sorting;  pub use self::pipeline::Pipeline; -use self::formats::Format; +use self::{aggregators::Aggregator, formats::Format};  /// Build an pipeline for the given command line options.  pub fn build_pipeline(opt: &Opt) -> Pipeline {      let stream = io::stdout(); -    let aggregator = aggregators::WriteThrough; +    let aggregator: Box<dyn Aggregator> = if let Some(sorting) = &opt.sorting { +        Box::new(aggregators::SortedOutput::new(sorting.clone())) +    } else { +        Box::new(aggregators::WriteThrough) +    };      let format: Box<dyn Format> = if opt.file_name_only {          Box::new(formats::FileOnly) @@ -23,5 +28,5 @@ pub fn build_pipeline(opt: &Opt) -> Pipeline {          })      }; -    Pipeline::new(Box::new(stream), format, Box::new(aggregator)) +    Pipeline::new(Box::new(stream), format, aggregator)  } diff --git a/src/output/pipeline.rs b/src/output/pipeline.rs index 9664928..9b7f461 100644 --- a/src/output/pipeline.rs +++ b/src/output/pipeline.rs @@ -22,8 +22,13 @@ impl Pipeline {          }      } -    pub fn push_item(&self, item: &LogResult) { +    pub fn push_item(&self, item: LogResult) {          let mut writer = self.writer.lock().unwrap();          self.aggregator.push_item(item, &*self.format, &mut *writer);      } + +    pub fn finish(self) { +        let mut writer = self.writer.lock().unwrap(); +        self.aggregator.finish(&*self.format, &mut *writer); +    }  } diff --git a/src/output/sorting.rs b/src/output/sorting.rs new file mode 100644 index 0000000..f46a95c --- /dev/null +++ b/src/output/sorting.rs @@ -0,0 +1,229 @@ +//! Definitions and functions to produce sorted output. + +use std::{ +    cmp::Ordering, +    fmt::{self, Display, Formatter}, +    str::FromStr, +}; + +use itertools::Itertools; +use thiserror::Error; + +use crate::LogResult; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Error)] +#[error("an invalid sorting component was given")] +pub struct InvalidComponent; + +/// A [`Component`][Component] is anything that can be used to sort the result by. +/// +/// Note that some components may not provide a true "sorting" (is Skorvald coming before Dhuum or +/// not?), but are more of a convenience that allows to group logs of the same component together. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Component { +    /// Sort the result date. +    Date, +    /// Sort by the boss. +    Boss, +    /// Sort by the outcome. +    Outcome, +    /// Sort based on whether the Challenge Mote was active or not. +    ChallengeMote, + +    /// Sort by the given component but in reverse direction. +    Reverse(Box<Component>), +} + +impl FromStr for Component { +    type Err = InvalidComponent; + +    fn from_str(s: &str) -> Result<Self, Self::Err> { +        if s.starts_with('~') { +            return s[1..].parse().map(|c| Component::Reverse(Box::new(c))); +        } +        match &s.to_lowercase() as &str { +            "date" => Ok(Component::Date), +            "boss" => Ok(Component::Boss), +            "outcome" => Ok(Component::Outcome), +            "cm" => Ok(Component::ChallengeMote), +            _ => Err(InvalidComponent), +        } +    } +} + +impl Display for Component { +    fn fmt(&self, f: &mut Formatter) -> fmt::Result { +        match self { +            Component::Date => write!(f, "date"), +            Component::Boss => write!(f, "boss"), +            Component::Outcome => write!(f, "outcome"), +            Component::ChallengeMote => write!(f, "cm"), +            Component::Reverse(ref inner) => write!(f, "~{}", inner), +        } +    } +} + +fn boss_id(log: &LogResult) -> u32 { +    log.boss.map(|x| x as u32).unwrap_or(0) +} + +impl Component { +    pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering { +        match self { +            Component::Date => lhs.time.cmp(&rhs.time), +            Component::Boss => boss_id(lhs).cmp(&boss_id(rhs)), +            Component::Outcome => lhs.outcome.cmp(&rhs.outcome), +            Component::ChallengeMote => lhs.is_cm.cmp(&rhs.is_cm), +            Component::Reverse(ref inner) => inner.cmp(lhs, rhs).reverse(), +        } +    } +} + +/// A sorting. +/// +/// The sorting goes lexicographically, in the sense that if the first chosen component is the same +/// for two elements, the next component will be used to compare two logs. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +pub struct Sorting(Vec<Component>); + +impl FromStr for Sorting { +    type Err = InvalidComponent; + +    fn from_str(s: &str) -> Result<Self, Self::Err> { +        if s == "" { +            return Ok(Sorting::default()); +        } +        let parts = s.split(','); +        parts +            .map(FromStr::from_str) +            .collect::<Result<Vec<Component>, _>>() +            .map(Sorting::new) +    } +} + +impl Display for Sorting { +    fn fmt(&self, f: &mut Formatter) -> fmt::Result { +        let string = self.0.iter().map(ToString::to_string).join(","); +        f.write_str(&string) +    } +} + +impl Sorting { +    pub fn new(components: Vec<Component>) -> Sorting { +        Sorting(components) +    } + +    pub fn cmp(&self, lhs: &LogResult, rhs: &LogResult) -> Ordering { +        for component in &self.0 { +            let result = component.cmp(lhs, rhs); +            if result != Ordering::Equal { +                return result; +            } +        } +        Ordering::Equal +    } +} + +#[cfg(test)] +mod tests { +    use super::super::FightOutcome; +    use super::*; + +    use chrono::prelude::*; +    use evtclib::Boss as B; + +    #[test] +    fn test_parse_component() { +        assert_eq!("date".parse(), Ok(Component::Date)); +        assert_eq!("boss".parse(), Ok(Component::Boss)); +        assert_eq!("outcome".parse(), Ok(Component::Outcome)); +        assert_eq!("cm".parse(), Ok(Component::ChallengeMote)); + +        assert_eq!("foobar".parse::<Component>(), Err(InvalidComponent)); + +        assert_eq!( +            "~date".parse(), +            Ok(Component::Reverse(Box::new(Component::Date))) +        ); +    } + +    #[test] +    fn test_parse_sorting() { +        use Component::*; +        assert_eq!("date".parse(), Ok(Sorting::new(vec![Date]))); +        assert_eq!("date,boss".parse(), Ok(Sorting::new(vec![Date, Boss]))); +        assert_eq!( +            "date,~boss".parse(), +            Ok(Sorting::new(vec![Date, Reverse(Box::new(Boss))])) +        ); +        assert_eq!("".parse(), Ok(Sorting::default())); +    } + +    #[test] +    fn test_sorting_cmp() { +        use Component::*; + +        let logs: &[&LogResult] = &[ +            &LogResult { +                log_file: "".into(), +                time: Utc.ymd(2020, 4, 3).and_hms(12, 0, 0), +                boss: Some(B::Dhuum), +                players: vec![], +                outcome: FightOutcome::Success, +                is_cm: false, +            }, +            &LogResult { +                log_file: "".into(), +                time: Utc.ymd(2020, 4, 3).and_hms(13, 0, 0), +                boss: Some(B::Dhuum), +                players: vec![], +                outcome: FightOutcome::Success, +                is_cm: false, +            }, +            &LogResult { +                log_file: "".into(), +                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), +                boss: Some(B::Dhuum), +                players: vec![], +                outcome: FightOutcome::Success, +                is_cm: false, +            }, +            &LogResult { +                log_file: "".into(), +                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), +                boss: Some(B::Qadim), +                players: vec![], +                outcome: FightOutcome::Success, +                is_cm: false, +            }, +            &LogResult { +                log_file: "".into(), +                time: Utc.ymd(2020, 4, 3).and_hms(11, 0, 0), +                boss: Some(B::Dhuum), +                players: vec![], +                outcome: FightOutcome::Success, +                is_cm: false, +            }, +        ]; + +        let sortings: &[(&[Component], &[&LogResult])] = &[ +            (&[Date], &[logs[2], logs[3], logs[4], logs[0], logs[1]]), +            ( +                &[Reverse(Box::new(Date))], +                &[logs[1], logs[0], logs[2], logs[3], logs[4]], +            ), +            (&[Boss], &[logs[0], logs[1], logs[2], logs[4], logs[3]]), +            ( +                &[Boss, Date], +                &[logs[2], logs[4], logs[0], logs[1], logs[3]], +            ), +        ]; + +        for (sorting, expected) in sortings { +            let mut data = logs.to_vec(); +            let sorting = Sorting::new(sorting.to_vec()); +            data.sort_by(|a, b| sorting.cmp(a, b)); +            assert_eq!(&data, expected, "Sorting with {:?} failed", sorting); +        } +    } +} | 
