diff options
-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())); + } +} |