From b49d7b79959fd2bb3894be686add50131ff7043e Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Sun, 5 Mar 2023 15:02:33 +0100 Subject: [PATCH] Evaluate in place expressions This solution gives similar performance results than the tree based one, mostly because of the intermediate vectors. --- board-shared/src/ai.rs | 16 ++-- board-shared/src/expr.rs | 166 +++++++++++++++++++++++++++++++++++++- board-shared/src/lexer.rs | 21 +++-- board-shared/src/tile.rs | 9 +++ 4 files changed, 196 insertions(+), 16 deletions(-) diff --git a/board-shared/src/ai.rs b/board-shared/src/ai.rs index 438cf18..3009c4e 100644 --- a/board-shared/src/ai.rs +++ b/board-shared/src/ai.rs @@ -1,7 +1,6 @@ -use crate::expr::are_valid_expressions; +use crate::expr::is_valid_guess_of_tokens; use crate::game::Hand; -use crate::lexer::lexer; -use crate::parser::parse; +use crate::lexer::{lexer_reuse, Token}; use crate::tile::{Digit, Operator, Tile}; use itertools::Itertools; @@ -37,6 +36,7 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec> { } let mut trial: Vec = Vec::with_capacity(numbers.len() + operators.len() + 1); + let mut tokens: Vec = Vec::with_capacity(numbers.len() + operators.len() + 1); // Generate all possible permutations, with an increasing number of tiles for nb_digits in 2..=numbers.len() { @@ -47,14 +47,12 @@ pub fn generate_valid_combinations(hand: &Hand) -> Vec> { for nb_operators in 0..=(nb_digits - 2) { for operators in operators.iter().permutations(nb_operators) { merge_expression(&digits, &operators, equals_idx, &mut trial); - if let Ok(tokens) = lexer(&trial) { - if let Ok(expressions) = parse(&tokens) { - if are_valid_expressions(&expressions) { - combinations.push(trial.clone()); - } - } + lexer_reuse(&trial, &mut tokens); + if is_valid_guess_of_tokens(&tokens) { + combinations.push(trial.clone()); } trial.clear(); + tokens.clear(); } } } diff --git a/board-shared/src/expr.rs b/board-shared/src/expr.rs index bf19feb..fee4f1a 100644 --- a/board-shared/src/expr.rs +++ b/board-shared/src/expr.rs @@ -1,10 +1,11 @@ use crate::board::Board; -use crate::lexer::lexer; +use crate::lexer::{lexer, DecimalToken, Token}; use crate::parser; use crate::parser::{Expression, Expressions}; use crate::position::Position2d; use crate::tile::{Operator, Tile}; +/// Evaluates a single expression syntax tree. pub fn calculate(expr: &Expression) -> f64 { match expr { Expression::Digit(value) => *value as f64, @@ -22,6 +23,7 @@ pub fn calculate(expr: &Expression) -> f64 { } } +/// Evaluates a vector of expression syntax trees. pub fn are_valid_expressions(expr: &Expressions) -> bool { let mut res: Option = None; for expr in expr { @@ -37,6 +39,31 @@ pub fn are_valid_expressions(expr: &Expressions) -> bool { res.is_some() } +/// Determines if the given tokens are a valid equation. +pub fn is_valid_guess_of_tokens(tokens: &[Token]) -> bool { + let mut res: Option = None; + for part in tokens.split(|token| matches!(token, Token::Equals)) { + let rpn = shunting_yard(part); + if let Ok(rpn) = rpn { + let value = evaluate_rpn(&rpn); + if let Ok(value) = value { + if let Some(res) = res { + if res != value { + return false; + } + } else { + res = Some(value); + } + } else { + return false; + } + } else { + return false; + } + } + res.is_some() +} + pub fn is_valid_guess(board: &Board, positions: &[Position2d]) -> Result { let tiles = positions .iter() @@ -44,11 +71,74 @@ pub fn is_valid_guess(board: &Board, positions: &[Position2d]) -> Result>>() .ok_or(())?; - let tokens = lexer(&tiles)?; + let tokens = lexer(&tiles); let expressions = parser::parse(&tokens)?; Ok(are_valid_expressions(&expressions)) } +/// Convert an infix expression to a postfix expression. +fn shunting_yard(tokens: &[Token]) -> Result, ()> { + let mut operator_stack: Vec = Vec::with_capacity(tokens.len()); + let mut output: Vec = Vec::with_capacity(tokens.len()); + + for token in tokens { + match token { + Token::NumberLiteral(num) => output.push(DecimalToken::NumberLiteral(*num as f64)), + Token::Operator(op) => { + while let Some(DecimalToken::Operator(top_op)) = operator_stack.last() { + if top_op.precedence() >= op.precedence() { + output.push(operator_stack.pop().unwrap()); + } else { + break; + } + } + operator_stack.push(DecimalToken::Operator(*op)); + } + Token::LeftParen => operator_stack.push(DecimalToken::LeftParen), + Token::RightParen => { + while let Some(top_op) = operator_stack.pop() { + if let DecimalToken::LeftParen = top_op { + break; + } else { + output.push(top_op); + } + } + } + _ => panic!("Unexpected token: {:?}", token), + } + } + + while let Some(top_op) = operator_stack.pop() { + output.push(top_op); + } + + Ok(output) +} + +/// Evaluate a postfix expression. +fn evaluate_rpn(tokens: &[DecimalToken]) -> Result { + let mut stack = Vec::new(); + for token in tokens { + match token { + DecimalToken::NumberLiteral(num) => stack.push(*num), + DecimalToken::Operator(op) => { + let right = stack.pop().ok_or(())?; + let left = stack.pop().ok_or(())?; + let result = match op { + Operator::Add => left + right, + Operator::Subtract => left - right, + Operator::Multiply => left * right, + Operator::Divide => left / right, + }; + stack.push(result); + } + _ => panic!("Unexpected token: {:?}", token), + } + } + + stack.pop().ok_or(()) +} + #[cfg(test)] mod tests { use super::*; @@ -89,4 +179,76 @@ mod tests { ]; assert!(!are_valid_expressions(&expr)); } + + #[test] + fn shunting_yard_sample() { + let tokens = vec![ + Token::NumberLiteral(5), + Token::Operator(Operator::Multiply), + Token::NumberLiteral(9), + Token::Operator(Operator::Subtract), + Token::NumberLiteral(2), + ]; + let res = shunting_yard(&tokens).expect("Failed to evaluate"); + assert_eq!( + res, + vec![ + DecimalToken::NumberLiteral(5.), + DecimalToken::NumberLiteral(9.), + DecimalToken::Operator(Operator::Multiply), + DecimalToken::NumberLiteral(2.), + DecimalToken::Operator(Operator::Subtract), + ] + ); + } + + #[test] + fn shunting_yard_precedence() { + let tokens = vec![ + Token::NumberLiteral(1), + Token::Operator(Operator::Add), + Token::NumberLiteral(2), + Token::Operator(Operator::Multiply), + Token::NumberLiteral(3), + Token::Operator(Operator::Add), + Token::NumberLiteral(4), + ]; + let res = shunting_yard(&tokens).expect("Failed to evaluate"); + assert_eq!( + res, + vec![ + DecimalToken::NumberLiteral(1.), + DecimalToken::NumberLiteral(2.), + DecimalToken::NumberLiteral(3.), + DecimalToken::Operator(Operator::Multiply), + DecimalToken::Operator(Operator::Add), + DecimalToken::NumberLiteral(4.), + DecimalToken::Operator(Operator::Add), + ] + ); + } + + #[test] + fn is_valid_guess_of_tiles_equals() { + let tokens = vec![ + Token::NumberLiteral(4), + Token::Operator(Operator::Subtract), + Token::NumberLiteral(1), + Token::Equals, + Token::NumberLiteral(3), + ]; + assert!(is_valid_guess_of_tokens(&tokens)); + } + + #[test] + fn is_valid_guess_of_tiles_not_equals() { + let tokens = vec![ + Token::NumberLiteral(8), + Token::Operator(Operator::Divide), + Token::NumberLiteral(4), + Token::Equals, + Token::NumberLiteral(5), + ]; + assert!(!is_valid_guess_of_tokens(&tokens)); + } } diff --git a/board-shared/src/lexer.rs b/board-shared/src/lexer.rs index e2b4f82..e8eb994 100644 --- a/board-shared/src/lexer.rs +++ b/board-shared/src/lexer.rs @@ -10,6 +10,13 @@ pub enum Token { Equals, } +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum DecimalToken { + NumberLiteral(f64), + Operator(Operator), + LeftParen, +} + impl fmt::Display for Token { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -23,9 +30,14 @@ impl fmt::Display for Token { } /// Tokenize a sequence of tiles into tokens. -pub fn lexer(input: &[Tile]) -> Result, ()> { +pub fn lexer(input: &[Tile]) -> Vec { let mut result = Vec::new(); + lexer_reuse(input, &mut result); + result +} +/// Tokenize a sequence of tiles into tokens. +pub fn lexer_reuse(input: &[Tile], result: &mut Vec) { let mut it = input.iter().peekable(); while let Some(&c) = it.peek() { match c { @@ -62,7 +74,6 @@ pub fn lexer(input: &[Tile]) -> Result, ()> { } } } - Ok(result) } #[cfg(test)] @@ -88,7 +99,7 @@ mod tests { Token::NumberLiteral(456), Token::Equals, ]; - assert_eq!(lexer(&input).unwrap(), expected); + assert_eq!(lexer(&input), expected); } #[test] @@ -117,7 +128,7 @@ mod tests { Token::NumberLiteral(3), Token::RightParen, ]; - assert_eq!(lexer(&input).unwrap(), expected); + assert_eq!(lexer(&input), expected); } #[test] @@ -141,6 +152,6 @@ mod tests { Token::NumberLiteral(1234), Token::RightParen, ]; - assert_eq!(lexer(&input).unwrap(), expected); + assert_eq!(lexer(&input), expected); } } diff --git a/board-shared/src/tile.rs b/board-shared/src/tile.rs index ee3d0cf..8f88bb1 100644 --- a/board-shared/src/tile.rs +++ b/board-shared/src/tile.rs @@ -68,6 +68,15 @@ pub enum Operator { Divide, } +impl Operator { + pub fn precedence(&self) -> u8 { + match self { + Operator::Add | Operator::Subtract => 1, + Operator::Multiply | Operator::Divide => 2, + } + } +} + impl fmt::Display for Operator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self {