aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel <kingdread@gmx.de>2020-05-14 16:13:28 +0200
committerDaniel <kingdread@gmx.de>2020-05-14 16:13:28 +0200
commitc973f2ddb47721254f6f5a81e4c45f0c72d793fd (patch)
tree4aa7c00b87eec09ee1faca957be508efe3dbbb65
parent331d6b1762d1d9431b210fc98a495d56ad7a1cd1 (diff)
parente2d23d4b76000263e9f939637353bbc4bb9289fd (diff)
downloadraidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.tar.gz
raidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.tar.bz2
raidgrep-c973f2ddb47721254f6f5a81e4c45f0c72d793fd.zip
Merge branch 'sorted-output'
-rw-r--r--raidgrep.1.asciidoc12
-rw-r--r--src/main.rs24
-rw-r--r--src/output/aggregators.rs48
-rw-r--r--src/output/mod.rs11
-rw-r--r--src/output/pipeline.rs7
-rw-r--r--src/output/sorting.rs229
6 files changed, 314 insertions, 17 deletions
diff --git a/raidgrep.1.asciidoc b/raidgrep.1.asciidoc
index 30e0b2f..584713e 100644
--- a/raidgrep.1.asciidoc
+++ b/raidgrep.1.asciidoc
@@ -48,6 +48,18 @@ command line:
*-d* 'path', *--dir* 'path'::
Path to the folder with logs [default: .]
+*-s* 'sorting', *--sort* 'sorting'::
+ Sort the output according to the given fields. Valid fields are 'boss',
+ 'date', 'cm' and 'outcome'. Fields can be comma-separated, then later
+ fields are used to break ties. Fields can be prefixed by ~ to indicate
+ sorting in the opposite direction. +
+ +
+ Note that using this option will cause raidgrep to buffer all results until
+ the search has finished, which means that no output will happen until every
+ file has been searched. It can also lead to a higher memory consumption,
+ based on how many logs are matched. Therefore, it is advised that you do
+ not use this option unless needed. +
+
== PREDICATES
The following predicates can be used after the options. Note that they should
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);
+ }
+ }
+}