//! 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 based on the fight duration. Duration, /// Sort by the given component but in reverse direction. Reverse(Box), } impl FromStr for Component { type Err = InvalidComponent; fn from_str(s: &str) -> Result { if let Some(stripped) = s.strip_prefix('~') { return stripped.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), "duration" => Ok(Component::Duration), _ => 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::Duration => write!(f, "duration"), Component::Reverse(ref inner) => write!(f, "~{}", inner), } } } fn boss_id(log: &LogResult) -> u32 { log.encounter.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::Duration => lhs.duration.cmp(&rhs.duration), 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); impl FromStr for Sorting { type Err = InvalidComponent; fn from_str(s: &str) -> Result { if s == "" { return Ok(Sorting::default()); } let parts = s.split(','); parts .map(FromStr::from_str) .collect::, _>>() .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) -> 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!("duration".parse(), Ok(Component::Duration)); assert_eq!("foobar".parse::(), 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 duration = chrono::Duration::zero(); let logs: &[&LogResult] = &[ &LogResult { log_file: "".into(), time: Utc.ymd(2020, 4, 3).and_hms(12, 0, 0), duration, 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), duration, 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), duration, 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), duration, 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), duration, 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); } } }