//! 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), } impl FromStr for Component { type Err = InvalidComponent; fn from_str(s: &str) -> Result { 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); 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::*; #[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::(), 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())); } }