diff options
Diffstat (limited to 'src/output/sorting.rs')
-rw-r--r-- | src/output/sorting.rs | 229 |
1 files changed, 229 insertions, 0 deletions
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); + } + } +} |