diff --git a/src/commands/eval/mod.rs b/src/commands/eval/mod.rs new file mode 100644 index 0000000..207d1d7 --- /dev/null +++ b/src/commands/eval/mod.rs @@ -0,0 +1,23 @@ +use crate::common::{Context, Error}; + +mod tokenizer; +mod parse; + +fn evaluate(expr: &str) -> Result { + let tokens = tokenizer::Token::tokenize(expr)?; + let mut tokens = tokens.iter(); + + let tree = parse::ParseTree::new(&mut tokens)?; + + Ok(tree.evaluate()) +} + +/// Evaluates an expression (uses Polish Notation) +#[poise::command(slash_command, prefix_command)] +pub async fn eval(ctx: Context<'_>, + #[rest] + expr: String) -> Result<(), Error> +{ + ctx.reply(format!("{}", evaluate(&expr)?)).await?; + Ok(()) +} diff --git a/src/commands/eval/parse.rs b/src/commands/eval/parse.rs new file mode 100644 index 0000000..2e3304b --- /dev/null +++ b/src/commands/eval/parse.rs @@ -0,0 +1,57 @@ +use std::error::Error; +use std::fmt::Display; +use super::tokenizer::{Token, Op}; + +#[derive(Debug)] +pub enum ParseTree { + Leaf(Token), + Branch(Op, Box, Box), +} + +#[derive(Debug, Clone)] +pub enum ParseError { + UnexpectedEndInput, +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::UnexpectedEndInput => write!(f, "Input ended unexpectedly"), + } + } +} + +impl Error for ParseError {} + +impl ParseTree { + pub fn new<'a, I: Iterator>(tokens: &mut I) -> Result { + if let Some(token) = tokens.next() { + match token { + Token::Scalar(_) | Token::Identifier(_) => Ok(Self::Leaf(token.clone())), + Token::Operator(op) => { + let left = ParseTree::new(tokens)?; + let right = ParseTree::new(tokens)?; + + Ok(Self::Branch(op.clone(), Box::new(left), Box::new(right))) + } + } + } else { + Err(ParseError::UnexpectedEndInput) + } + } + + pub fn evaluate(&self) -> f64 { + match self { + ParseTree::Leaf(Token::Scalar(value)) => *value, + ParseTree::Leaf(Token::Identifier(_)) => unimplemented!(), + ParseTree::Leaf(Token::Operator(_)) => panic!("This absolutely should not happen"), + ParseTree::Branch(op, left, right) => match op { + Op::Add => left.evaluate() + right.evaluate(), + Op::Sub => left.evaluate() - right.evaluate(), + Op::Mul => left.evaluate() * right.evaluate(), + Op::Div => left.evaluate() / right.evaluate(), + Op::Exp => left.evaluate().powf(right.evaluate()), + } + } + } +} \ No newline at end of file diff --git a/src/commands/eval/tokenizer.rs b/src/commands/eval/tokenizer.rs new file mode 100644 index 0000000..a686b8d --- /dev/null +++ b/src/commands/eval/tokenizer.rs @@ -0,0 +1,94 @@ +use std::error; +use crate::common::Error; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub enum Op { + Add, + Sub, + Mul, + Div, + Exp, +} + +#[derive(Debug, Clone)] +pub enum Token { + Identifier(String), + Scalar(f64), + Operator(Op), +} + +#[derive(Debug, Clone)] +struct ParseError(String); + +impl Display for ParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl error::Error for ParseError { + fn description(&self) -> &str { + &self.0 + } +} + +impl Token { + pub fn identifier(str: String) -> Token { + Token::Identifier(str) + } + + pub fn scalar(value: f64) -> Token { + Token::Scalar(value) + } + + pub fn add() -> Token { + Token::Operator(Op::Add) + } + + pub fn sub() -> Token { + Token::Operator(Op::Sub) + } + + pub fn mul() -> Token { + Token::Operator(Op::Mul) + } + + pub fn div() -> Token { + Token::Operator(Op::Div) + } + + pub fn exp() -> Token { + Token::Operator(Op::Exp) + } + + pub fn tokenize(s: &str) -> Result, Error> { + s.split_whitespace().map(Token::from_str).collect() + } +} + +impl FromStr for Token { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + // First check if s is an operator + "+" => Ok(Token::add()), + "-" => Ok(Token::sub()), + "*" => Ok(Token::mul()), + "**" => Ok(Token::exp()), + "/" => Ok(Token::div()), + _ => { + if s.starts_with(|c| char::is_digit(c, 10)) { + Ok(Token::scalar(s.parse()?)) + } else if s.starts_with(char::is_alphabetic) + && s.chars().skip(1).all(char::is_alphanumeric) { + Ok(Token::identifier(s.to_string())) + } else { + Err(Box::new(ParseError(format!("Failed to parse \"{}\"", s)))) + } + } + } + } +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0aa68cd..9e50758 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -5,6 +5,7 @@ mod ping; mod dox; mod yeehaw; mod gambling; +mod eval; pub fn commands() -> Vec> { vec![ @@ -14,5 +15,6 @@ pub fn commands() -> Vec> { gambling::balance::balance(), gambling::give::give(), gambling::wager::wager(), + eval::eval(), ] }