//! This module contains specific filters that operate on log files. //! //! This is the "base unit", as each file corresponds to one log. Filters on other items (such as //! players) have to be lifted into log filters first. use super::{ super::{EarlyLogResult, FightOutcome, LogResult}, Filter, Inclusion, }; use std::collections::HashSet; use evtclib::{Encounter, GameMode}; use chrono::{DateTime, Datelike, Utc, Weekday}; /// Filter trait used for filters that operate on complete logs. pub trait LogFilter = Filter; #[derive(Debug, Clone)] struct BossFilter(HashSet); impl Filter for BossFilter { fn filter_early(&self, early_log: &EarlyLogResult) -> Inclusion { let boss = Encounter::from_header_id(early_log.evtc.header.combat_id); boss.map(|b| self.0.contains(&b).into()) .unwrap_or(Inclusion::Exclude) } fn filter(&self, log: &LogResult) -> bool { log.encounter.map(|b| self.0.contains(&b)).unwrap_or(false) } } /// A `LogFilter` that only accepts logs with one of the given bosses. pub fn encounter(bosses: HashSet) -> Box { Box::new(BossFilter(bosses)) } #[derive(Debug, Clone)] struct GameModeFilter(HashSet); impl Filter for GameModeFilter { fn filter_early(&self, early_log: &EarlyLogResult) -> Inclusion { let encounter_id = early_log.evtc.header.combat_id; // Special WvW encounter const GENERIC_ENCOUNTER_ID: u16 = 1; let mode = if encounter_id == GENERIC_ENCOUNTER_ID { Some(GameMode::WvW) } else { Encounter::from_header_id(encounter_id).map(Encounter::game_mode) }; mode.map(|m| self.0.contains(&m).into()) .unwrap_or(Inclusion::Exclude) } fn filter(&self, log: &LogResult) -> bool { log.game_mode.map(|m| self.0.contains(&m)).unwrap_or(false) } } /// A [`LogFilter`] that only accepts logs with the given game mode. pub fn game_mode(game_modes: HashSet) -> Box { Box::new(GameModeFilter(game_modes)) } #[derive(Debug, Clone)] struct OutcomeFilter(HashSet); impl Filter for OutcomeFilter { fn filter(&self, log: &LogResult) -> bool { self.0.contains(&log.outcome) } } /// A `LogFilter` that only accepts logs with one of the given outcomes. /// /// See also [`success`][success] and [`wipe`][wipe]. pub fn outcome(outcomes: HashSet) -> Box { Box::new(OutcomeFilter(outcomes)) } /// A `LogFilter` that only accepts successful logs. /// /// See also [`outcome`][outcome] and [`wipe`][wipe]. pub fn success() -> Box { let mut outcomes = HashSet::new(); outcomes.insert(FightOutcome::Success); outcome(outcomes) } /// A `LogFilter` that only accepts failed logs. /// /// See also [`outcome`][outcome] and [`success`][wipe]. pub fn wipe() -> Box { let mut outcomes = HashSet::new(); outcomes.insert(FightOutcome::Wipe); outcome(outcomes) } #[derive(Debug, Clone)] struct WeekdayFilter(HashSet); impl Filter for WeekdayFilter { fn filter(&self, log: &LogResult) -> bool { self.0.contains(&log.time.weekday()) } } /// A `LogFilter` that only accepts logs if they were done on one of the given weekdays. pub fn weekday(weekdays: HashSet) -> Box { Box::new(WeekdayFilter(weekdays)) } #[derive(Debug, Clone)] struct TimeFilter(Option>, Option>); impl Filter for TimeFilter { fn filter(&self, log: &LogResult) -> bool { time_is_between(log.time, self.0, self.1) } } /// Check if the given time is after `after` but before `before`. /// /// If one of the bounds is `None`, the time is always in bounds w.r.t. that bound. fn time_is_between( time: DateTime, after: Option>, before: Option>, ) -> bool { let after_ok = match after { Some(after) => after <= time, None => true, }; let before_ok = match before { Some(before) => before >= time, None => true, }; after_ok && before_ok } /// A `LogFilter` that only accepts logs in the given time frame. /// /// Compared to [`-time`][super::values::time], this filter ignores the file name. This can result /// in more accurate results if you renamed logs, but if also leads to a worse runtime. pub fn log_time(lower: Option>, upper: Option>) -> Box { Box::new(TimeFilter(lower, upper)) } /// Only include logs after the given date, but ignore the file name for date calculations. pub fn log_after(when: DateTime) -> Box { log_time(Some(when), None) } /// Only include logs before the given date, but ignore the file name for date calculations. pub fn log_before(when: DateTime) -> Box { log_time(None, Some(when)) } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct CmFilter; impl Filter for CmFilter { fn filter(&self, log: &LogResult) -> bool { log.is_cm } } /// A filter that only includes logs that had the Challenge Mote turned on. pub fn challenge_mote() -> Box { Box::new(CmFilter) } #[cfg(test)] mod tests { use super::*; use chrono::TimeZone; #[test] fn test_time_is_between() { assert!(time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), None, None, )); assert!(time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), Some(Utc.ymd(1955, 11, 5).and_hms(5, 0, 0)), None, )); assert!(time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), None, Some(Utc.ymd(1955, 11, 5).and_hms(7, 0, 0)), )); assert!(time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), Some(Utc.ymd(1955, 11, 5).and_hms(5, 0, 0)), Some(Utc.ymd(1955, 11, 5).and_hms(7, 0, 0)), )); assert!(!time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), Some(Utc.ymd(1955, 11, 5).and_hms(7, 0, 0)), None, )); assert!(!time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), None, Some(Utc.ymd(1955, 11, 5).and_hms(5, 0, 0)), )); assert!(!time_is_between( Utc.ymd(1955, 11, 5).and_hms(6, 15, 0), Some(Utc.ymd(1955, 11, 5).and_hms(5, 0, 0)), Some(Utc.ymd(1955, 11, 5).and_hms(6, 0, 0)), )); } }