use aurora_core::{AuroraResult, Pipeline, Value};
#[derive(Debug, Clone, Copy, PartialEq)]
enum Token {
Number(f64),
Plus,
Minus,
Star,
Slash,
Percent,
Caret,
LParen,
RParen,
}
pub fn calc_eval(expr: &[String]) -> AuroraResult<Pipeline> {
let input = expr.join(" ");
let tokens = tokenize(&input)?;
let result = evaluate(&tokens)?;
Ok(Pipeline::table(
vec!["expression".into(), "result".into()],
vec![vec![
Value::String(input),
Value::Float(result),
]],
))
}
fn tokenize(input: &str) -> AuroraResult<Vec<Token>> {
let mut tokens = Vec::new();
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
if c.is_ascii_digit() || c == '.' {
let mut num = String::new();
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
num.push(chars[i]);
i += 1;
}
let n: f64 = num.parse()
.map_err(|_| aurora_core::AuroraError::InvalidInput(
format!("invalid number: {}", num)
))?;
tokens.push(Token::Number(n));
continue;
}
match c {
'+' => tokens.push(Token::Plus),
'-' => tokens.push(Token::Minus),
'*' => tokens.push(Token::Star),
'/' => tokens.push(Token::Slash),
'%' => tokens.push(Token::Percent),
'^' => tokens.push(Token::Caret),
'(' => tokens.push(Token::LParen),
')' => tokens.push(Token::RParen),
_ => return Err(aurora_core::AuroraError::InvalidInput(
format!("unexpected character: {}", c)
)),
}
i += 1;
}
Ok(tokens)
}
fn precedence(op: Token) -> i32 {
match op {
Token::Plus | Token::Minus => 1,
Token::Star | Token::Slash | Token::Percent => 2,
Token::Caret => 3,
_ => 0,
}
}
fn apply_op(op: Token, a: f64, b: f64) -> f64 {
match op {
Token::Plus => a + b,
Token::Minus => a - b,
Token::Star => a * b,
Token::Slash => a / b,
Token::Percent => a % b,
Token::Caret => a.powf(b),
_ => a,
}
}
fn evaluate(tokens: &[Token]) -> AuroraResult<f64> {
let mut values: Vec<f64> = Vec::new();
let mut ops: Vec<Token> = Vec::new();
let mut i = 0;
while i < tokens.len() {
match tokens[i] {
Token::Number(n) => values.push(n),
Token::LParen => ops.push(Token::LParen),
Token::RParen => {
loop {
let op = ops.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("mismatched parentheses".into())
})?;
match op {
Token::LParen => break,
_ => {
let b = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
let a = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
values.push(apply_op(op, a, b));
}
}
}
}
ref op @ (Token::Plus | Token::Minus | Token::Star | Token::Slash | Token::Percent | Token::Caret) => {
while let Some(top) = ops.last() {
if *top == Token::LParen {
break;
}
if precedence(*top) >= precedence(*op) {
let top_op = ops.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
let b = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
let a = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
values.push(apply_op(top_op, a, b));
} else {
break;
}
}
ops.push(*op);
}
}
i += 1;
}
while let Some(op) = ops.pop() {
let b = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
let a = values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("expression error".into())
})?;
values.push(apply_op(op, a, b));
}
values.pop().ok_or_else(|| {
aurora_core::AuroraError::InvalidInput("empty expression".into())
})
}