use std::{
collections::HashMap,
error::Error,
fmt::{self, Display, Formatter, Result as FmtResult},
str::FromStr,
};
use console::style;
use regex::RegexBuilder;
use crate::tr;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum Field {
Event,
Site,
Date,
Round,
Black,
White,
Result,
BlackElo,
WhiteElo,
BlackRatingDiff,
WhiteRatingDiff,
BlackTitle,
WhiteTitle,
ECO,
Opening,
TimeControl,
Termination,
TotalPlyCount,
ScidFlags,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Operator {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
Re,
ReNeg,
}
#[derive(Clone, Debug)]
pub struct FilterExpression {
field: Field,
operator: Operator,
value: String,
regex: Option<regex::Regex>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum LogicalOperator {
And,
Or,
}
#[derive(Clone, Debug, PartialEq)]
pub enum FilterComponent {
Expression(FilterExpression),
LogicalOperator(LogicalOperator),
OpenParenthesis,
CloseParenthesis,
}
impl FromStr for Field {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Event" => Ok(Field::Event),
"Site" => Ok(Field::Site),
"Date" => Ok(Field::Date),
"UTCDate" => Ok(Field::Date),
"Round" => Ok(Field::Round),
"White" => Ok(Field::White),
"Black" => Ok(Field::Black),
"Result" => Ok(Field::Result),
"BlackElo" => Ok(Field::BlackElo),
"WhiteElo" => Ok(Field::WhiteElo),
"BlackRatingDiff" => Ok(Field::BlackRatingDiff),
"WhiteRatingDiff" => Ok(Field::WhiteRatingDiff),
"BlackTitle" => Ok(Field::BlackTitle),
"WhiteTitle" => Ok(Field::WhiteTitle),
"ECO" => Ok(Field::ECO),
"Opening" => Ok(Field::Opening),
"TimeControl" => Ok(Field::TimeControl),
"Termination" => Ok(Field::Termination),
"TotalPlyCount" => Ok(Field::TotalPlyCount),
"ScidFlags" => Ok(Field::ScidFlags),
_ => Err(()),
}
}
}
impl FromStr for Operator {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"=" => Ok(Operator::Eq),
"!=" => Ok(Operator::Ne),
"<" => Ok(Operator::Lt),
"<=" => Ok(Operator::Le),
">" => Ok(Operator::Gt),
">=" => Ok(Operator::Ge),
"=~" => Ok(Operator::Re),
"!~" => Ok(Operator::ReNeg),
_ => Err(()),
}
}
}
impl fmt::Display for Operator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let operator_str = match self {
Operator::Eq => "=",
Operator::Ne => "!=",
Operator::Lt => "<",
Operator::Le => "<=",
Operator::Gt => ">",
Operator::Ge => ">=",
Operator::Re => "=~",
Operator::ReNeg => "!~",
};
write!(f, "{}", operator_str)
}
}
impl fmt::Display for LogicalOperator {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let operator_str = match self {
LogicalOperator::And => "AND",
LogicalOperator::Or => "OR",
};
write!(f, "{}", operator_str)
}
}
impl fmt::Display for Field {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let field_str = match self {
Field::Event => "Event",
Field::Site => "Site",
Field::Date => "Date",
Field::Round => "Round",
Field::Black => "Black",
Field::White => "White",
Field::Result => "Result",
Field::BlackElo => "BlackElo",
Field::WhiteElo => "WhiteElo",
Field::BlackRatingDiff => "BlackRatingDiff",
Field::WhiteRatingDiff => "WhiteRatingDiff",
Field::BlackTitle => "BlackTitle",
Field::WhiteTitle => "WhiteTitle",
Field::ECO => "ECO",
Field::Opening => "Opening",
Field::TimeControl => "TimeControl",
Field::Termination => "Termination",
Field::TotalPlyCount => "TotalPlyCount",
Field::ScidFlags => "ScidFlags",
};
write!(f, "{}", field_str)
}
}
impl Field {
pub fn from_utf8(input: &[u8]) -> Option<Self> {
match input {
b"Event" => Some(Field::Event),
b"Site" => Some(Field::Site),
b"Date" => Some(Field::Date),
b"UTCDate" => Some(Field::Date),
b"Round" => Some(Field::Round),
b"Black" => Some(Field::Black),
b"White" => Some(Field::White),
b"Result" => Some(Field::Result),
b"BlackElo" => Some(Field::BlackElo),
b"WhiteElo" => Some(Field::WhiteElo),
b"BlackRatingDiff" => Some(Field::BlackRatingDiff),
b"WhiteRatingDiff" => Some(Field::WhiteRatingDiff),
b"BlackTitle" => Some(Field::BlackTitle),
b"WhiteTitle" => Some(Field::WhiteTitle),
b"ECO" => Some(Field::ECO),
b"Opening" => Some(Field::Opening),
b"TimeControl" => Some(Field::TimeControl),
b"Termination" => Some(Field::Termination),
b"TotalPlyCount" => Some(Field::TotalPlyCount),
b"ScidFlags" => Some(Field::ScidFlags),
_ => None,
}
}
}
impl PartialEq for FilterExpression {
fn eq(&self, other: &Self) -> bool {
self.field == other.field && self.operator == other.operator && self.value == other.value
}
}
#[derive(Debug)]
pub enum FilterExpressionParseError {
UnsupportedOperator(String),
InvalidSyntax(String),
EmptyExpression,
InvalidField(String),
MissingOperator,
InvalidOperator(String),
MissingValue,
InvalidRegex(String),
}
impl Display for FilterExpressionParseError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
FilterExpressionParseError::UnsupportedOperator(op) => {
write!(f, "{}", tr!("Unsupported operator: `{}'", op))
}
FilterExpressionParseError::InvalidSyntax(expr) => {
write!(
f,
"{}",
tr!("Invalid syntax in filter expression: `{}'", expr)
)
}
FilterExpressionParseError::EmptyExpression => {
write!(f, "{}", tr!("Filter expression cannot be empty"))
}
FilterExpressionParseError::InvalidField(field) => {
write!(
f,
"{}",
tr!("Invalid field in filter expression: `{}'", field)
)
}
FilterExpressionParseError::MissingOperator => {
write!(f, "{}", tr!("Missing operator"))
}
FilterExpressionParseError::InvalidOperator(op) => {
write!(
f,
"{}",
tr!("Invalid operator in field expression: `{}'", op)
)
}
FilterExpressionParseError::MissingValue => {
write!(f, "{}", tr!("Missing value"))
}
FilterExpressionParseError::InvalidRegex(regex) => {
write!(
f,
"{}",
tr!("Invalid regex in field expression: `{}'", regex)
)
}
}
}
}
impl Error for FilterExpressionParseError {}
impl From<()> for FilterExpressionParseError {
fn from(_: ()) -> Self {
FilterExpressionParseError::InvalidField(
tr!("Player, Elo, Title or RatingDiff expected").to_string(),
)
}
}
pub fn parse_filter_expression(
filter_expr: &str,
) -> Result<Vec<FilterComponent>, FilterExpressionParseError> {
let mut components = Vec::new();
let mut tokens = filter_expr.split_whitespace();
let mut current_token = tokens.next();
while let Some(token) = current_token {
match token {
"(" => {
components.push(FilterComponent::OpenParenthesis);
}
")" => {
components.push(FilterComponent::CloseParenthesis);
}
"AND" | "OR" => {
let logical_operator = match token {
"AND" => LogicalOperator::And,
"OR" => LogicalOperator::Or,
_ => unreachable!(),
};
components.push(FilterComponent::LogicalOperator(logical_operator));
}
_ => {
let field_str = token;
let operator_str = tokens
.next()
.ok_or(FilterExpressionParseError::MissingOperator)?;
let operator = match Operator::from_str(operator_str) {
Ok(operator) => operator,
Err(_) => {
return Err(FilterExpressionParseError::InvalidOperator(
operator_str.to_string(),
))
}
};
let value = tokens
.next()
.ok_or(FilterExpressionParseError::MissingValue)?;
if field_str == "Player"
|| field_str == "Elo"
|| field_str == "Title"
|| field_str == "RatingDiff"
{
let player_dependent_expression =
build_player_dependent_expression(field_str, operator, value)?;
components.extend(player_dependent_expression);
} else {
let field = match Field::from_str(field_str) {
Ok(field) => field,
Err(_) => {
return Err(FilterExpressionParseError::InvalidField(
field_str.to_string(),
))
}
};
let regex = if operator == Operator::Re || operator == Operator::ReNeg {
match RegexBuilder::new(value).case_insensitive(true).build() {
Ok(re) => Some(re),
Err(err) => {
eprintln!(
"Invalid regex in filter expression `{}': {}",
&value, err
);
return Err(FilterExpressionParseError::InvalidRegex(
value.to_string(),
));
}
}
} else {
None
};
let expression = FilterExpression {
field,
operator,
value: value.to_string(),
regex,
};
components.push(FilterComponent::Expression(expression));
}
}
}
current_token = tokens.next();
}
Ok(components)
}
pub fn game_passes_filter(
filter_expr: &[FilterComponent],
game_tags: &HashMap<Field, String>,
verbose: u8,
) -> bool {
fn evaluate_expression(
filter_expr: &[FilterComponent],
game_tags: &HashMap<Field, String>,
index: &mut usize,
verbose: u8,
) -> bool {
let mut result = true;
let mut current_operator = None;
while *index < filter_expr.len() {
match &filter_expr[*index] {
FilterComponent::OpenParenthesis => {
*index += 1;
let inner_result = evaluate_expression(filter_expr, game_tags, index, verbose);
result = match current_operator {
Some(LogicalOperator::And) => result && inner_result,
Some(LogicalOperator::Or) => result || inner_result,
None => inner_result,
};
current_operator = None;
}
FilterComponent::CloseParenthesis => {
*index += 1;
break;
}
FilterComponent::Expression(FilterExpression {
field,
operator,
value,
regex,
}) => {
let tag_value = game_tags.get(field);
let comparison_result = match tag_value {
Some(tag_value) => compare_tag_value(tag_value, operator, value, regex),
None => false,
};
result = match current_operator {
Some(LogicalOperator::And) => result && comparison_result,
Some(LogicalOperator::Or) => result || comparison_result,
None => comparison_result,
};
if verbose > 1 && result {
eprintln!(
"{}",
tr!(
"Field: {}, Operator: {}, LogicalOperator: {} Value: {}, Tag Value: {}",
style(field).magenta(),
style(operator).bold().cyan(),
style(current_operator.map_or("None".to_string(), |v| v.to_string()))
.bold()
.yellow(),
style(value).green(),
tag_value.map_or(style("?").bold().red().to_string(), |v| style(v)
.bold()
.green()
.to_string()))
);
}
*index += 1;
current_operator = None;
}
FilterComponent::LogicalOperator(operator) => {
current_operator = Some(*operator);
*index += 1;
}
}
}
result
}
let mut index = 0;
evaluate_expression(filter_expr, game_tags, &mut index, verbose)
}
fn build_player_dependent_expression(
field: &str,
operator: Operator,
value: &str,
) -> Result<Vec<FilterComponent>, FilterExpressionParseError> {
let field = if field == "Player" { "" } else { field };
let white_field = Field::from_str(&format!("White{}", field))?;
let black_field = Field::from_str(&format!("Black{}", field))?;
let operator_str = operator.to_string();
let expression = format!(
"( {} {} {} OR {} {} {} )",
white_field, operator_str, value, black_field, operator_str, value
);
parse_filter_expression(&expression)
}
fn compare_tag_value(
tag_value: &str,
operator: &Operator,
value: &str,
regex: &Option<regex::Regex>,
) -> bool {
match operator {
Operator::Eq => tag_value == value,
Operator::Ne => tag_value != value,
Operator::Lt => match (tag_value.parse::<i32>(), value.parse::<i32>()) {
(Ok(tag_val_num), Ok(value_num)) => tag_val_num < value_num,
_ => false,
},
Operator::Le => match (tag_value.parse::<i32>(), value.parse::<i32>()) {
(Ok(tag_val_num), Ok(value_num)) => tag_val_num <= value_num,
_ => false,
},
Operator::Gt => match (tag_value.parse::<i32>(), value.parse::<i32>()) {
(Ok(tag_val_num), Ok(value_num)) => tag_val_num > value_num,
_ => false,
},
Operator::Ge => match (tag_value.parse::<i32>(), value.parse::<i32>()) {
(Ok(tag_val_num), Ok(value_num)) => tag_val_num >= value_num,
_ => false,
},
Operator::Re => {
if let Some(re) = regex {
re.is_match(tag_value)
} else {
eprintln!(
"{}",
tr!(
"Error: regex is missing in filter expression for `{}'",
tag_value
)
);
false
}
}
Operator::ReNeg => {
if let Some(re) = regex {
!re.is_match(tag_value)
} else {
eprintln!(
"{}",
tr!(
"Error: regex is missing in filter expression for `{}'",
tag_value
)
);
false
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use regex::Regex;
use super::*;
fn build_game_tags() -> HashMap<Field, String> {
let mut game_tags = HashMap::new();
game_tags.insert(Field::Event, "World Championship".to_string());
game_tags.insert(Field::White, "Magnus Carlsen".to_string());
game_tags.insert(Field::Black, "Fabiano Caruana".to_string());
game_tags.insert(Field::Result, "1-0".to_string());
game_tags.insert(Field::ECO, "C65".to_string());
game_tags
}
#[test]
fn test_parse_filter_expression() {
let filter_expr = "Event =~ World AND White =~ Carlsen AND Result = 1-0 OR ECO = C65";
let parsed_expr = parse_filter_expression(filter_expr).unwrap();
assert_eq!(
parsed_expr,
vec![
FilterComponent::Expression(FilterExpression {
field: Field::Event,
operator: Operator::Re,
value: "World".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::And),
FilterComponent::Expression(FilterExpression {
field: Field::White,
operator: Operator::Re,
value: "Carlsen".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::And),
FilterComponent::Expression(FilterExpression {
field: Field::Result,
operator: Operator::Eq,
value: "1-0".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::Or),
FilterComponent::Expression(FilterExpression {
field: Field::ECO,
operator: Operator::Eq,
value: "C65".to_string(),
regex: None,
}),
]
);
}
#[test]
fn test_game_passes_filter() {
let filter_expr =
"Event =~ World AND White =~ Carlsen AND ( Result = 1/2-1/2 OR ECO = C65 )";
let parsed_expr = parse_filter_expression(filter_expr).unwrap();
let game_tags = build_game_tags();
assert!(game_passes_filter(&parsed_expr, &game_tags, 0));
}
#[test]
fn test_game_does_not_pass_filter() {
let filter_expr = "Event =~ World AND White =~ Carlsen AND ( Result = 0-1 OR ECO = B33 )";
let parsed_expr = parse_filter_expression(filter_expr).unwrap();
let game_tags = build_game_tags();
assert!(!game_passes_filter(&parsed_expr, &game_tags, 0));
}
#[test]
fn test_parse_filter_expression_player() {
let filter_expr = "Player =~ ^M";
let parsed = parse_filter_expression(filter_expr).unwrap();
assert_eq!(
parsed,
vec![
FilterComponent::OpenParenthesis,
FilterComponent::Expression(FilterExpression {
field: Field::White,
operator: Operator::Re,
value: "^M".to_string(),
regex: Some(Regex::new("(?i)^M").unwrap()),
}),
FilterComponent::LogicalOperator(LogicalOperator::Or),
FilterComponent::Expression(FilterExpression {
field: Field::Black,
operator: Operator::Re,
value: "^M".to_string(),
regex: Some(Regex::new("(?i)^M").unwrap()),
}),
FilterComponent::CloseParenthesis
]
);
}
#[test]
fn test_parse_filter_expression_elo() {
let filter_expr = "Elo >= 2400";
let parsed = parse_filter_expression(filter_expr).unwrap();
assert_eq!(
parsed,
vec![
FilterComponent::OpenParenthesis,
FilterComponent::Expression(FilterExpression {
field: Field::WhiteElo,
operator: Operator::Ge,
value: "2400".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::Or),
FilterComponent::Expression(FilterExpression {
field: Field::BlackElo,
operator: Operator::Ge,
value: "2400".to_string(),
regex: None,
}),
FilterComponent::CloseParenthesis
]
);
}
#[test]
fn test_parse_filter_expression_title() {
let filter_expr = "Title = GM";
let parsed = parse_filter_expression(filter_expr).unwrap();
assert_eq!(
parsed,
vec![
FilterComponent::OpenParenthesis,
FilterComponent::Expression(FilterExpression {
field: Field::WhiteTitle,
operator: Operator::Eq,
value: "GM".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::Or),
FilterComponent::Expression(FilterExpression {
field: Field::BlackTitle,
operator: Operator::Eq,
value: "GM".to_string(),
regex: None,
}),
FilterComponent::CloseParenthesis
]
);
}
#[test]
fn test_parse_filter_expression_ratingdiff() {
let filter_expr = "RatingDiff < 10";
let parsed = parse_filter_expression(filter_expr).unwrap();
assert_eq!(
parsed,
vec![
FilterComponent::OpenParenthesis,
FilterComponent::Expression(FilterExpression {
field: Field::WhiteRatingDiff,
operator: Operator::Lt,
value: "10".to_string(),
regex: None,
}),
FilterComponent::LogicalOperator(LogicalOperator::Or),
FilterComponent::Expression(FilterExpression {
field: Field::BlackRatingDiff,
operator: Operator::Lt,
value: "10".to_string(),
regex: None,
}),
FilterComponent::CloseParenthesis
]
);
}
}