//! 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, ffi::OsStr}; use evtclib::Boss; use chrono::{DateTime, Datelike, Local, TimeZone, Utc, Weekday}; use num_traits::FromPrimitive as _; use once_cell::sync::Lazy; use regex::Regex; /// The regular expression used to extract datetimes from filenames. static DATE_REGEX: Lazy = Lazy::new(|| Regex::new(r"\d{8}-\d{6}").unwrap()); /// 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 = Boss::from_u16(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.boss.map(|b| self.0.contains(&b)).unwrap_or(false) } } /// A `LogFilter` that only accepts logs with one of the given bosses. pub fn boss(bosses: HashSet) -> Box { Box::new(BossFilter(bosses)) } #[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>, bool); impl Filter for TimeFilter { fn filter_early(&self, early_log: &EarlyLogResult) -> Inclusion { // Ignore the filename heuristic if the user wishes so. if !self.2 { return Inclusion::Unknown; } early_log .log_file .file_name() .and_then(datetime_from_filename) .map(|d| time_is_between(d, self.0, self.1)) .map(Into::into) .unwrap_or(Inclusion::Unknown) } 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 } /// Try to extract the log time from the filename. /// /// This expects the filename to have the datetime in the pattern `YYYYmmdd-HHMMSS` somewhere in /// it. fn datetime_from_filename(name: &OsStr) -> Option> { let date_match = DATE_REGEX.find(name.to_str()?)?; let local_time = Local .datetime_from_str(date_match.as_str(), "%Y%m%d-%H%M%S") .ok()?; Some(local_time.with_timezone(&Utc)) } /// A `LogFilter` that only accepts logs in the given time frame. /// /// If a bound is not given, -Infinity is assumed for the lower bound, and Infinity for the upper /// bound. pub fn time(lower: Option>, upper: Option>) -> Box { Box::new(TimeFilter(lower, upper, true)) } /// A `LogFilter` that only accepts logs after the given date. /// /// Also see [`time`][time] and [`before`][before]. pub fn after(when: DateTime) -> Box { time(Some(when), None) } /// A `LogFilter` that only accepts logs before the given date. /// /// Also see [`time`][time] and [`after`][after]. pub fn before(when: DateTime) -> Box { time(None, Some(when)) } /// A `LogFilter` that only accepts logs in the given time frame. /// /// Compared to [`time`][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, false)) } /// Like [`after`][after], but ignores the file name for date calculations. pub fn log_after(when: DateTime) -> Box { log_time(Some(when), None) } /// Like [`before`][before], but ignores 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::*; #[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)), )); } }