diff options
author | Daniel <kingdread@gmx.de> | 2020-05-13 13:49:43 +0200 |
---|---|---|
committer | Daniel <kingdread@gmx.de> | 2020-05-13 13:49:43 +0200 |
commit | fb2a6088dcc7b57a2c1ac93ec6a8fbcc52584734 (patch) | |
tree | 66979361f60dad90f6c9b0eff7c5bf20404357e8 | |
parent | 331d6b1762d1d9431b210fc98a495d56ad7a1cd1 (diff) | |
download | raidgrep-fb2a6088dcc7b57a2c1ac93ec6a8fbcc52584734.tar.gz raidgrep-fb2a6088dcc7b57a2c1ac93ec6a8fbcc52584734.tar.bz2 raidgrep-fb2a6088dcc7b57a2c1ac93ec6a8fbcc52584734.zip |
first attempt at sorting output
This does currently not work yet, as we cannot call .finish() on dyn
Aggregator. This needs to be adjusted.
However, this provides the basic infrastructure for producing sorted
output, including the required command line parsing.
-rw-r--r-- | src/main.rs | 16 | ||||
-rw-r--r-- | src/output/aggregators.rs | 46 | ||||
-rw-r--r-- | src/output/mod.rs | 11 | ||||
-rw-r--r-- | src/output/pipeline.rs | 7 | ||||
-rw-r--r-- | src/output/sorting.rs | 148 |
5 files changed, 215 insertions, 13 deletions
diff --git a/src/main.rs b/src/main.rs index ee1c232..ad996ea 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,10 @@ pub struct Opt { #[structopt(long = "no-color")] no_color: bool, + /// Sort the output. + #[structopt(short = "s", long = "sort")] + sorting: Option<Sorting>, + /// Print more debugging information to stderr. #[structopt(long = "debug")] debug: bool, @@ -150,7 +155,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,7 +382,8 @@ 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); + let pipeline = output::build_pipeline(opt); + let pipeline_ref = &pipeline; rayon::scope(|s| { let walker = WalkDir::new(&opt.path); for entry in walker { @@ -391,7 +397,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 +406,9 @@ fn grep(opt: &Opt, filter: &dyn LogFilter) -> Result<()> { }); } Ok(()) - }) + }) as Result<()>; + pipeline.finish(); + Ok(()) } /// Search the given single log. diff --git a/src/output/aggregators.rs b/src/output/aggregators.rs index 1b04af3..4fa2558 100644 --- a/src/output/aggregators.rs +++ b/src/output/aggregators.rs @@ -6,12 +6,15 @@ //! //! 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 push_item(&self, item: LogResult, format: &dyn Format, stream: &mut dyn Write); fn finish(self, format: &dyn Format, stream: &mut dyn Write); } @@ -20,11 +23,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) {} } + +/// 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, 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..18ab84b 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::{formats::Format, aggregators::Aggregator}; /// 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..8b271a9 --- /dev/null +++ b/src/output/sorting.rs @@ -0,0 +1,148 @@ +//! 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::*; + + #[test] + fn 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 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())); + } +} |