From 0cb54858732ec6db0ef1de3f937ea41bec5ec7ae Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 28 Aug 2024 22:08:03 +0200 Subject: implement sorting of logs (not available as a setting yet) --- src/categories.rs | 101 ++++++++++++++++---- src/logbag.rs | 278 ++++++++++++++++++++++++++++++++++++++++++------------ src/matrix.rs | 2 +- 3 files changed, 301 insertions(+), 80 deletions(-) (limited to 'src') diff --git a/src/categories.rs b/src/categories.rs index de4f5e6..1713d87 100644 --- a/src/categories.rs +++ b/src/categories.rs @@ -1,48 +1,115 @@ use evtclib::{Encounter, GameMode, Log}; +// There is no canonical order of "categories", so we'll do +// * WvW +// * Raid wings ascending +// * Strike +// * Fractals descending +// * Training area +// * Unknown +// Subject to change, it's only used to sort the message (if enabled). +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)] +pub enum Category { + WvW, + Wing1, + Wing2, + Wing3, + Wing4, + Wing5, + Wing6, + Wing7, + Strike, + SunquaPeak, + ShatteredObservatory, + Nightmare, + SpecialForcesTrainingArea, + #[default] + Unknown, +} + +macro_rules! category_strings { + ($(($item:path, $str:literal),)*) => { + impl std::fmt::Display for Category { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let repr = match *self { + $($item => $str),* + }; + write!(f, "{}", repr) + } + } + + impl std::str::FromStr for Category { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + $($str => Ok($item),)* + _ => Err(()), + } + } + } + } +} + +category_strings! { + (Category::WvW, "World versus World"), + (Category::Wing1, "Wing 1 (Spirit Vale)"), + (Category::Wing2, "Wing 2 (Salvation Pass)"), + (Category::Wing3, "Wing 3 (Stronghold of the Faithful)"), + (Category::Wing4, "Wing 4 (Bastion of the Penitent)"), + (Category::Wing5, "Wing 5 (Hall of Chains)"), + (Category::Wing6, "Wing 6 (Mythwright Gambit)"), + (Category::Wing7, "Wing 7 (Key of Ahdashim)"), + (Category::SunquaPeak, "100 CM (Sunqua Peak)"), + (Category::ShatteredObservatory, "99 CM (Shattered Observatory)"), + (Category::Nightmare, "98 CM (Nightmare)"), + (Category::Strike, "Strike Mission"), + (Category::SpecialForcesTrainingArea, "Special Forces Training Area"), + (Category::Unknown, "Unknown"), +} + pub trait Categorizable { - fn category(&self) -> &'static str; + fn category(&self) -> Category; } impl Categorizable for Log { - fn category(&self) -> &'static str { + fn category(&self) -> Category { if self.game_mode() == Some(GameMode::WvW) { - return "World versus World"; + return Category::WvW; } if let Some(encounter) = self.encounter() { match encounter { Encounter::ValeGuardian | Encounter::Gorseval | Encounter::Sabetha => { - "Wing 1 (Spirit Vale)" + Category::Wing1 } Encounter::Slothasor | Encounter::BanditTrio | Encounter::Matthias => { - "Wing 2 (Salvation Pass)" + Category::Wing2 } Encounter::KeepConstruct | Encounter::TwistedCastle | Encounter::Xera => { - "Wing 3 (Stronghold of the Faithful)" + Category::Wing3 } Encounter::Cairn | Encounter::MursaatOverseer | Encounter::Samarog - | Encounter::Deimos => "Wing 4 (Bastion of the Penitent)", + | Encounter::Deimos => Category::Wing4, Encounter::SoullessHorror | Encounter::RiverOfSouls | Encounter::BrokenKing | Encounter::EaterOfSouls | Encounter::StatueOfDarkness - | Encounter::VoiceInTheVoid => "Wing 5 (Hall of Chains)", + | Encounter::VoiceInTheVoid => Category::Wing5, Encounter::ConjuredAmalgamate | Encounter::TwinLargos | Encounter::Qadim => { - "Wing 6 (Mythwright Gambit)" + Category::Wing6 } Encounter::CardinalAdina | Encounter::CardinalSabir - | Encounter::QadimThePeerless => "Wing 7 (Key of Ahdashim)", + | Encounter::QadimThePeerless => Category::Wing7, - Encounter::Ai => "100 CM (Sunqua Peak)", + Encounter::Ai => Category::SunquaPeak, Encounter::Skorvald | Encounter::Artsariiv | Encounter::Arkk => { - "99 CM (Shattered Observatory)" + Category::ShatteredObservatory } - Encounter::MAMA | Encounter::Siax | Encounter::Ensolyss => "98 CM (Nightmare)", + Encounter::MAMA | Encounter::Siax | Encounter::Ensolyss => Category::Nightmare, Encounter::IcebroodConstruct | Encounter::SuperKodanBrothers @@ -52,16 +119,16 @@ impl Categorizable for Log { | Encounter::CaptainMaiTrin | Encounter::Ankka | Encounter::MinisterLi - | Encounter::Dragonvoid => "Strike Mission", + | Encounter::Dragonvoid => Category::Strike, Encounter::StandardKittyGolem | Encounter::MediumKittyGolem - | Encounter::LargeKittyGolem => "Special Forces Training Area", + | Encounter::LargeKittyGolem => Category::SpecialForcesTrainingArea, - _ => "Unknown", + _ => Category::Unknown, } } else { - "Unknown" + Category::Unknown } } } diff --git a/src/logbag.rs b/src/logbag.rs index 362bee5..f173d6e 100644 --- a/src/logbag.rs +++ b/src/logbag.rs @@ -1,8 +1,10 @@ -use std::iter; -use std::str::FromStr; +use super::categories::Category; -use evtclib::{Log, Outcome}; +use std::{cmp::Ordering, sync::OnceLock, str::FromStr}; + +use evtclib::{Encounter, Log, Outcome}; use itertools::Itertools; +use regex::Regex; /// A [`LogBag`] is a struct that holds multiple logs in their categories. /// @@ -11,7 +13,7 @@ use itertools::Itertools; /// them and we can just handle arbitrary data. #[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct LogBag { - data: Vec<(String, Vec)>, + data: Vec<(Category, Vec)>, } // Conditional compilation makes it hard to really use all the code, so we just allow dead code @@ -24,30 +26,54 @@ impl LogBag { } /// Return an iterator over all available categories. - pub fn categories(&self) -> impl Iterator { - self.data.iter().map(|x| &x.0 as &str) + pub fn categories(&self) -> impl Iterator + '_ { + self.data.iter().map(|x| x.0) } /// Return an iterator over (category, items). - pub fn iter(&self) -> impl Iterator)> { + pub fn iter(&self) -> impl Iterator)> { self.data .iter() - .map(|(cat, lines)| (cat as &str, lines.iter().map(|l| l as &str))) + .map(|(cat, lines)| (*cat, lines.iter().map(|l| l as &str))) } /// Insert an item into the given category. /// /// If the category does not exist yet, it will be appended at the bottom. - pub fn insert(&mut self, category: &str, line: String) { + pub fn insert(&mut self, category: Category, line: String) { for (cat, lines) in self.data.iter_mut() { - if cat == category { + if *cat == category { lines.push(line); return; } } // When we reach here, we don't have the category yet, so we gotta insert it. - self.data.push((String::from(category), vec![line])); + self.data.push((category, vec![line])); + } + + /// Sort the categories and logs. + pub fn sort(&mut self) { + // First sort the categories, the Ord impl of `Category` takes care here + self.data.sort(); + // Then sort the lines within the categories + for (_, ref mut lines) in self.data.iter_mut() { + lines.sort_by(|a, b| { + let (encounter_a, date_a, url_a) = info_from_line(&a); + let (encounter_b, date_b, url_b) = info_from_line(&b); + match (encounter_a, encounter_b) { + (None, None) => date_a.cmp(&date_b), + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + (Some(encounter_a), Some(encounter_b)) => encounter_a.partial_cmp(&encounter_b) + .or_else(|| order_strikes(encounter_a, encounter_b)) + // at this point, just give us a stable order + .unwrap_or((encounter_a as u16).cmp(&(encounter_b as u16))) + .then(date_a.cmp(&date_b)) + .then(url_a.cmp(&url_b)), + } + }); + } } /// Tries to parse the given text as a plain [`LogBag`]. @@ -68,7 +94,7 @@ impl LogBag { /// The output of this can be fed back into [`LogBag::parse_plain`] to round-trip. pub fn render_plain(&self) -> String { self.iter() - .map(|(category, lines)| iter::once(category).chain(lines).join("\n")) + .map(|(category, mut lines)| category.to_string() + "\n" + &lines.join("\n")) .join("\n\n") } @@ -104,18 +130,18 @@ impl FromStr for LogBag { let mut lines = chunk.split('\n'); let category = lines.next().unwrap(); ( - category.to_string(), + category.parse::().unwrap_or_default(), lines.map(ToString::to_string).collect::>(), ) }) - .filter(|(cat, lines)| !cat.is_empty() && !lines.is_empty()) + .filter(|(_, lines)| !lines.is_empty()) .collect(); Ok(LogBag { data }) } } -impl From)>> for LogBag { - fn from(data: Vec<(String, Vec)>) -> Self { +impl From)>> for LogBag { + fn from(data: Vec<(Category, Vec)>) -> Self { LogBag { data } } } @@ -130,6 +156,100 @@ pub fn state_emoji(log: &Log) -> &'static str { } } +// I don't want to pull in something like `chrono` just to represent a local datetime. Therefore, +// we simply use two integers: The date in the format YYYYMMDD (which automatically sorts +// correctly), and the time in HHMMSS (which does as well). +fn info_from_line(line: &str) -> (Option, Option<(u32, u32)>, &str) { + static RE: OnceLock = OnceLock::new(); + let url_re = RE.get_or_init(|| { + Regex::new("http(s?)://[^ ]+(?P\\d{8})-(?P