aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/csl.rs34
-rw-r--r--src/filters.rs396
-rw-r--r--src/main.rs43
3 files changed, 397 insertions, 76 deletions
diff --git a/src/csl.rs b/src/csl.rs
index e7d84f3..ac20ada 100644
--- a/src/csl.rs
+++ b/src/csl.rs
@@ -1,14 +1,14 @@
use std::collections::HashSet;
+use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
-use std::fmt;
-use super::{SearchField, FightOutcome};
+use super::{FightOutcome, SearchField};
use chrono::Weekday;
use evtclib::statistics::gamedata::Boss;
pub trait Variants: Copy {
- type Output: Iterator<Item=Self>;
+ type Output: Iterator<Item = Self>;
fn variants() -> Self::Output;
}
@@ -79,18 +79,21 @@ impl<E: fmt::Display> fmt::Display for ParseError<E> {
}
impl<T> FromStr for CommaSeparatedList<T>
- where T: FromStr + Variants + Hash + Eq + fmt::Debug
+where
+ T: FromStr + Variants + Hash + Eq + fmt::Debug,
{
type Err = ParseError<T::Err>;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if input == "*" {
- Ok(CommaSeparatedList { values: T::variants().collect() })
+ Ok(CommaSeparatedList {
+ values: T::variants().collect(),
+ })
} else if input.starts_with(NEGATOR) {
let no_csl = CommaSeparatedList::from_str(&input[1..])?;
let all_values = T::variants().collect::<HashSet<_>>();
Ok(CommaSeparatedList {
- values: all_values.difference(&no_csl.values).cloned().collect()
+ values: all_values.difference(&no_csl.values).cloned().collect(),
})
} else {
let parts = input.split(DELIMITER);
@@ -104,9 +107,26 @@ impl<T> FromStr for CommaSeparatedList<T>
}
impl<T> CommaSeparatedList<T>
- where T: Hash + Eq + fmt::Debug
+where
+ T: Hash + Eq + fmt::Debug,
{
pub fn contains(&self, value: &T) -> bool {
self.values.contains(value)
}
+
+ pub fn values(&self) -> &HashSet<T> {
+ &self.values
+ }
+}
+
+// We allow implicit hasher because then it's a zero-cost conversion, as we're just unwrapping the
+// values.
+#[allow(clippy::implicit_hasher)]
+impl<T> From<CommaSeparatedList<T>> for HashSet<T>
+where
+ T: Hash + Eq + fmt::Debug,
+{
+ fn from(csl: CommaSeparatedList<T>) -> Self {
+ csl.values
+ }
}
diff --git a/src/filters.rs b/src/filters.rs
index cdd8f36..7da19ec 100644
--- a/src/filters.rs
+++ b/src/filters.rs
@@ -1,79 +1,361 @@
+#![allow(clippy::new_ret_no_self)]
+use std::collections::HashSet;
+use std::ops;
+
use evtclib::raw::parser::PartialEvtc;
use evtclib::statistics::gamedata::Boss;
use evtclib::{Agent, AgentName};
-use num_traits::FromPrimitive;
-
-use super::{guilds, LogResult, Opt, SearchField};
-
-use chrono::Datelike;
-
-/// Do filtering based on the character or account name.
-pub fn filter_name(evtc: &PartialEvtc, opt: &Opt) -> bool {
- for player in &evtc.agents {
- let fancy = Agent::from_raw(player);
- if let Ok(AgentName::Player {
- ref account_name,
- ref character_name,
- ..
- }) = fancy.as_ref().map(Agent::name)
- {
- if (opt.field.contains(&SearchField::Account) && opt.expression.is_match(account_name))
- || (opt.field.contains(&SearchField::Character)
- && opt.expression.is_match(character_name))
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive as _;
+
+use regex::Regex;
+
+use super::{guilds, FightOutcome, LogResult, SearchField, Weekday};
+
+use chrono::{Datelike, NaiveDateTime};
+
+/// Early filtering result.
+///
+/// This implements a [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic),
+/// similar to SQL's `NULL`.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, FromPrimitive)]
+#[repr(i8)]
+pub enum Inclusion {
+ /// The log should be included.
+ Include = 1,
+ /// The state is yet unknown.
+ Unknown = 0,
+ /// The log should be excluded.
+ Exclude = -1,
+}
+
+impl ops::Not for Inclusion {
+ type Output = Self;
+
+ fn not(self) -> Self::Output {
+ Inclusion::from_i8(-(self as i8)).unwrap()
+ }
+}
+
+impl ops::BitAnd<Inclusion> for Inclusion {
+ type Output = Self;
+
+ fn bitand(self, rhs: Inclusion) -> Self::Output {
+ Inclusion::from_i8((self as i8).min(rhs as i8)).unwrap()
+ }
+}
+
+impl ops::BitOr<Inclusion> for Inclusion {
+ type Output = Self;
+
+ fn bitor(self, rhs: Inclusion) -> Self::Output {
+ Inclusion::from_i8((self as i8).max(rhs as i8)).unwrap()
+ }
+}
+
+impl From<bool> for Inclusion {
+ fn from(data: bool) -> Self {
+ if data {
+ Inclusion::Include
+ } else {
+ Inclusion::Exclude
+ }
+ }
+}
+
+/// The main filter trait.
+///
+/// Filters are usually handled as a `Box<dyn Filter>`.
+pub trait Filter: Send + Sync {
+ /// Determine early (before processing all events) whether the log stands a chance to be
+ /// included.
+ ///
+ /// Note that you can return [Inclusion::Unkown] if this filter cannot determine yet a definite
+ /// answer.
+ fn filter_early(&self, _: &PartialEvtc) -> Inclusion {
+ Inclusion::Unknown
+ }
+
+ /// Return whether the log should be included, according to this filter.
+ fn filter(&self, log: &LogResult) -> bool;
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Const(pub bool);
+
+impl Const {
+ pub fn new(output: bool) -> Box<dyn Filter> {
+ Box::new(Const(output))
+ }
+}
+
+impl Filter for Const {
+ fn filter_early(&self, _: &PartialEvtc) -> Inclusion {
+ self.0.into()
+ }
+
+ fn filter(&self, _: &LogResult) -> bool {
+ self.0
+ }
+}
+
+struct AndFilter(Box<dyn Filter>, Box<dyn Filter>);
+
+impl Filter for AndFilter {
+ fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion {
+ let lhs = self.0.filter_early(partial_evtc);
+ // Short circuit behaviour
+ if lhs == Inclusion::Exclude {
+ Inclusion::Exclude
+ } else {
+ lhs & self.1.filter_early(partial_evtc)
+ }
+ }
+
+ fn filter(&self, log: &LogResult) -> bool {
+ self.0.filter(log) && self.1.filter(log)
+ }
+}
+
+impl ops::BitAnd<Box<dyn Filter>> for Box<dyn Filter> {
+ type Output = Box<dyn Filter>;
+
+ fn bitand(self, rhs: Box<dyn Filter>) -> Self::Output {
+ Box::new(AndFilter(self, rhs))
+ }
+}
+
+struct OrFilter(Box<dyn Filter>, Box<dyn Filter>);
+
+impl Filter for OrFilter {
+ fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion {
+ let lhs = self.0.filter_early(partial_evtc);
+ // Short circuit behaviour
+ if lhs == Inclusion::Include {
+ Inclusion::Include
+ } else {
+ lhs | self.1.filter_early(partial_evtc)
+ }
+ }
+
+ fn filter(&self, log: &LogResult) -> bool {
+ self.0.filter(log) || self.1.filter(log)
+ }
+}
+
+impl ops::BitOr<Box<dyn Filter>> for Box<dyn Filter> {
+ type Output = Box<dyn Filter>;
+
+ fn bitor(self, rhs: Box<dyn Filter>) -> Self::Output {
+ Box::new(OrFilter(self, rhs))
+ }
+}
+
+struct NotFilter(Box<dyn Filter>);
+
+impl Filter for NotFilter {
+ fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion {
+ !self.0.filter_early(partial_evtc)
+ }
+
+ fn filter(&self, log: &LogResult) -> bool {
+ !self.0.filter(log)
+ }
+}
+
+impl ops::Not for Box<dyn Filter> {
+ type Output = Box<dyn Filter>;
+
+ fn not(self) -> Self::Output {
+ Box::new(NotFilter(self))
+ }
+}
+
+// From here onwards, we have the specific filters
+
+/// Filter that filters according to the name.
+///
+/// The given SearchField determines in which field something should be searched.
+#[derive(Debug, Clone)]
+pub struct NameFilter(SearchField, Regex);
+
+impl NameFilter {
+ pub fn new(field: SearchField, regex: Regex) -> Box<dyn Filter> {
+ Box::new(NameFilter(field, regex))
+ }
+}
+
+impl Filter for NameFilter {
+ fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion {
+ if self.0 == SearchField::Guild {
+ return Inclusion::Unknown;
+ }
+
+ for player in &partial_evtc.agents {
+ let fancy = Agent::from_raw(player);
+ if let Ok(AgentName::Player {
+ ref account_name,
+ ref character_name,
+ ..
+ }) = fancy.as_ref().map(Agent::name)
{
- return true;
+ let field = match self.0 {
+ SearchField::Account => account_name,
+ SearchField::Character => character_name,
+ _ => unreachable!("We already checked for Guild earlier"),
+ };
+ if self.1.is_match(field) {
+ return Inclusion::Include;
+ }
+ }
+ }
+ Inclusion::Exclude
+ }
+
+ fn filter(&self, log: &LogResult) -> bool {
+ for player in &log.players {
+ match self.0 {
+ SearchField::Account if self.1.is_match(&player.account_name) => return true,
+ SearchField::Character if self.1.is_match(&player.character_name) => return true,
+ SearchField::Guild => {
+ let guild_ok = player
+ .guild_id
+ .as_ref()
+ .and_then(|id| guilds::lookup(id))
+ .map(|guild| self.1.is_match(guild.tag()) || self.1.is_match(guild.name()))
+ .unwrap_or(false);
+ if guild_ok {
+ return true;
+ }
+ }
+ _ => (),
}
}
+ false
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct BossFilter(HashSet<Boss>);
+
+impl BossFilter {
+ pub fn new(bosses: HashSet<Boss>) -> Box<dyn Filter> {
+ Box::new(BossFilter(bosses))
+ }
+}
+
+impl Filter for BossFilter {
+ fn filter_early(&self, partial_evtc: &PartialEvtc) -> Inclusion {
+ let boss = Boss::from_u16(partial_evtc.header.combat_id);
+ boss.map(|b| self.0.contains(&b).into())
+ .unwrap_or(Inclusion::Include)
+ }
+
+ fn filter(&self, log: &LogResult) -> bool {
+ let boss = Boss::from_u16(log.boss_id);
+ boss.map(|b| self.0.contains(&b)).unwrap_or(false)
}
- // Don't throw away the log yet if we are searching for guilds
- opt.field.contains(&SearchField::Guild)
}
-/// Do filtering based on the boss ID.
-pub fn filter_boss(evtc: &PartialEvtc, opt: &Opt) -> bool {
- let boss = Boss::from_u16(evtc.header.combat_id);
- boss.map(|b| opt.bosses.contains(&b)).unwrap_or(true)
+#[derive(Debug, Clone)]
+pub struct OutcomeFilter(HashSet<FightOutcome>);
+
+impl OutcomeFilter {
+ pub fn new(outcomes: HashSet<FightOutcome>) -> Box<dyn Filter> {
+ Box::new(OutcomeFilter(outcomes))
+ }
}
-/// Do filtering based on the fight outcome.
-pub fn filter_outcome(result: &LogResult, opt: &Opt) -> bool {
- opt.outcome.contains(&result.outcome)
+impl Filter for OutcomeFilter {
+ fn filter(&self, log: &LogResult) -> bool {
+ self.0.contains(&log.outcome)
+ }
}
-/// Do filtering based on the weekday of the fight.
-pub fn filter_weekday(result: &LogResult, opt: &Opt) -> bool {
- opt.weekdays.contains(&result.time.weekday())
+#[derive(Debug, Clone)]
+pub struct WeekdayFilter(HashSet<Weekday>);
+
+impl WeekdayFilter {
+ pub fn new(weekdays: HashSet<Weekday>) -> Box<dyn Filter> {
+ Box::new(WeekdayFilter(weekdays))
+ }
}
-/// Do filtering based on encounter time.
-pub fn filter_time(result: &LogResult, opt: &Opt) -> bool {
- let after_ok = match opt.after {
- Some(time) => time <= result.time,
- None => true,
- };
- let before_ok = match opt.before {
- Some(time) => time >= result.time,
- None => true,
- };
+impl Filter for WeekdayFilter {
+ fn filter(&self, log: &LogResult) -> bool {
+ self.0.contains(&log.time.weekday())
+ }
+}
- after_ok && before_ok
+#[derive(Debug, Clone)]
+pub struct TimeFilter(Option<NaiveDateTime>, Option<NaiveDateTime>);
+
+impl TimeFilter {
+ pub fn new(after: Option<NaiveDateTime>, before: Option<NaiveDateTime>) -> Box<dyn Filter> {
+ Box::new(TimeFilter(after, before))
+ }
}
-/// Do filtering based on the guilds.
-pub fn filter_guilds(result: &LogResult, opt: &Opt) -> bool {
- if !opt.guilds {
- return true;
+impl Filter for TimeFilter {
+ fn filter(&self, log: &LogResult) -> bool {
+ let after_ok = match self.0 {
+ Some(time) => time <= log.time,
+ None => true,
+ };
+ let before_ok = match self.1 {
+ Some(time) => time >= log.time,
+ None => true,
+ };
+
+ after_ok && before_ok
}
- if !opt.field.contains(&SearchField::Guild) {
- return true;
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_inclusion_not() {
+ use Inclusion::*;
+
+ assert_eq!(!Exclude, Include);
+ assert_eq!(!Include, Exclude);
+ assert_eq!(!Unknown, Unknown);
+ }
+
+ #[test]
+ fn test_inclusion_and() {
+ use Inclusion::*;
+
+ assert_eq!(Exclude & Exclude, Exclude);
+ assert_eq!(Exclude & Unknown, Exclude);
+ assert_eq!(Exclude & Include, Exclude);
+
+ assert_eq!(Unknown & Exclude, Exclude);
+ assert_eq!(Unknown & Unknown, Unknown);
+ assert_eq!(Unknown & Include, Unknown);
+
+ assert_eq!(Include & Exclude, Exclude);
+ assert_eq!(Include & Unknown, Unknown);
+ assert_eq!(Include & Include, Include);
+ }
+
+ #[test]
+ fn test_inclusion_or() {
+ use Inclusion::*;
+
+ assert_eq!(Exclude | Exclude, Exclude);
+ assert_eq!(Exclude | Unknown, Unknown);
+ assert_eq!(Exclude | Include, Include);
+
+ assert_eq!(Unknown | Exclude, Unknown);
+ assert_eq!(Unknown | Unknown, Unknown);
+ assert_eq!(Unknown | Include, Include);
+
+ assert_eq!(Include | Exclude, Include);
+ assert_eq!(Include | Unknown, Include);
+ assert_eq!(Include | Include, Include);
}
- result.players.iter().any(|player| {
- let guild = player.guild_id.as_ref().and_then(|id| guilds::lookup(id));
- if let Some(guild) = guild {
- opt.expression.is_match(guild.tag()) || opt.expression.is_match(guild.name())
- } else {
- false
- }
- })
}
diff --git a/src/main.rs b/src/main.rs
index 6b67875..41df732 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,15 +6,16 @@ use std::str::FromStr;
use anyhow::{anyhow, Result};
use chrono::{Duration, NaiveDateTime, Weekday};
+use log::debug;
use num_traits::cast::FromPrimitive;
use regex::Regex;
use structopt::StructOpt;
use walkdir::{DirEntry, WalkDir};
-use log::debug;
use evtclib::{AgentKind, AgentName, EventKind, Log};
mod filters;
+use filters::{Filter, Inclusion};
mod guilds;
mod logger;
mod output;
@@ -32,7 +33,6 @@ macro_rules! unwrap {
};
}
-
/// A program that allows you to search through all your evtc logs for specific
/// people.
#[derive(StructOpt, Debug)]
@@ -106,7 +106,7 @@ pub struct Opt {
/// A flag indicating which fields should be searched.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
-enum SearchField {
+pub enum SearchField {
/// Only search the account name.
Account,
/// Only search the character name.
@@ -135,6 +135,8 @@ pub struct LogResult {
log_file: PathBuf,
/// The time of the recording.
time: NaiveDateTime,
+ /// The numeric ID of the boss.
+ boss_id: u16,
/// The name of the boss.
boss_name: String,
/// A vector of all participating players.
@@ -258,16 +260,36 @@ fn is_log_file(entry: &DirEntry) -> bool {
.unwrap_or(false)
}
+fn build_filter(opt: &Opt) -> Box<dyn Filter> {
+ let mut filter = filters::Const::new(false);
+ for field in opt.field.values() {
+ filter = filter | filters::NameFilter::new(*field, opt.expression.clone());
+ }
+
+ if opt.invert {
+ filter = !filter;
+ }
+
+ filter = filter
+ & filters::BossFilter::new(opt.bosses.values().clone())
+ & filters::OutcomeFilter::new(opt.outcome.values().clone())
+ & filters::WeekdayFilter::new(opt.weekdays.values().clone())
+ & filters::TimeFilter::new(opt.after, opt.before);
+
+ filter
+}
+
/// Run the grep search with the given options.
fn grep(opt: &Opt) -> Result<()> {
let pipeline = &output::build_pipeline(opt);
+ let filter: &dyn Filter = &*build_filter(opt);
rayon::scope(|s| {
let walker = WalkDir::new(&opt.path);
for entry in walker {
let entry = entry?;
s.spawn(move |_| {
if is_log_file(&entry) {
- let search = search_log(&entry, opt);
+ let search = search_log(&entry, filter);
match search {
Ok(None) => (),
Ok(Some(result)) => pipeline.push_item(&result),
@@ -287,7 +309,7 @@ fn grep(opt: &Opt) -> Result<()> {
/// If the log matches, returns `Ok(Some(..))`.
/// If the log doesn't match, returns `Ok(None)`.
/// If there was a fatal error, returns `Err(..)`.
-fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {
+fn search_log(entry: &DirEntry, filter: &dyn Filter) -> Result<Option<LogResult>> {
let file_stream = BufReader::new(File::open(entry.path())?);
let is_zip = entry
.file_name()
@@ -302,10 +324,9 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {
let mut stream = wrapper.get_stream();
let partial = evtclib::raw::parser::parse_partial_file(&mut stream)?;
- let early_ok =
- filters::filter_name(&partial, opt) != opt.invert && filters::filter_boss(&partial, opt);
+ let early_ok = filter.filter_early(&partial);
- if !early_ok {
+ if early_ok == Inclusion::Exclude {
return Ok(None);
}
@@ -320,10 +341,7 @@ fn search_log(entry: &DirEntry, opt: &Opt) -> Result<Option<LogResult>> {
let info = extract_info(entry, &log);
- let take_log = filters::filter_outcome(&info, opt)
- && filters::filter_weekday(&info, opt)
- && filters::filter_time(&info, opt)
- && filters::filter_guilds(&info, opt);
+ let take_log = filter.filter(&info);
if take_log {
Ok(Some(info))
@@ -372,6 +390,7 @@ fn extract_info(entry: &DirEntry, log: &Log) -> LogResult {
LogResult {
log_file: entry.path().to_path_buf(),
time: NaiveDateTime::from_timestamp(i64::from(get_start_timestamp(log)), 0),
+ boss_id: log.boss_id(),
boss_name,
players,
outcome: get_fight_outcome(log),