diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 174526b..8fa3bcb 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -24,7 +24,7 @@ use crate::app::default_settings; use crate::app::menubar::menu_icon; use crate::app::tile::AppIndex; use crate::app::tile::elm::default_app_paths; -use crate::calculator::Expression; +use crate::calculator::Expr; use crate::clipboard::ClipBoardContentType; use crate::commands::Function; use crate::config::Config; @@ -158,13 +158,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.handle_search_query_changed(); if tile.results.is_empty() - && let Some(res) = Expression::from_str(&tile.query) + && let Some(res) = Expr::from_str(&tile.query).ok() { tile.results.push(App { - open_command: AppCommand::Function(Function::Calculate(res)), + open_command: AppCommand::Function(Function::Calculate(res.clone())), desc: RUSTCAST_DESC_NAME.to_string(), icons: None, - name: res.eval().to_string(), + name: res.eval().map(|x| x.to_string()).unwrap_or("".to_string()), name_lc: "".to_string(), }); } else if tile.results.is_empty() diff --git a/src/calculator.rs b/src/calculator.rs index c1f5029..6012028 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -1,114 +1,388 @@ -//! This handle the logic for the calculator in rustcast +/// A small expression parser/evaluator supporting: +/// - + - * / ^ with precedence +/// - parentheses +/// - unary +/- +/// - ln(x) +/// - log(x) (base 10) +/// - log(base, x) +/// +/// Examples: +/// "2 + 3*4" => 14 +/// "2^(1+2)" => 8 +/// "-(3 + 4)" => -7 +/// "ln(2.7182818)" => ~1 +/// "log(100)" => 2 +/// "log(2, 8)" => 3 + +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + Number(f64), + Unary { + op: UnaryOp, + rhs: Box, + }, + Binary { + op: BinOp, + lhs: Box, + rhs: Box, + }, + Func { + name: String, + args: Vec, + }, +} -/// A struct that represents an expression #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Expression { - pub first_num: f64, - pub operation: Operation, - pub second_num: f64, +pub enum UnaryOp { + Plus, + Minus, } -/// An enum that represents the different operations that can be performed on an expression #[derive(Debug, Clone, Copy, PartialEq)] -pub enum Operation { - Addition, - Subtraction, - Multiplication, - Division, - Power, +pub enum BinOp { + Add, + Sub, + Mul, + Div, + Pow, } -impl Expression { - /// This evaluates the expression - pub fn eval(&self) -> f64 { - match self.operation { - Operation::Addition => self.first_num + self.second_num, - Operation::Subtraction => self.first_num - self.second_num, - Operation::Multiplication => self.first_num * self.second_num, - Operation::Division => self.first_num / self.second_num, - Operation::Power => self.first_num.powf(self.second_num), +impl Expr { + pub fn eval(&self) -> Option { + use BinOp::*; + use UnaryOp::*; + match self { + Expr::Number(x) => Some(*x), + + Expr::Unary { op, rhs } => { + let v = rhs.eval()?; + Some(match op { + Plus => v, + Minus => -v, + }) + } + + Expr::Binary { op, lhs, rhs } => { + let a = lhs.eval()?; + let b = rhs.eval()?; + match op { + Add => Some(a + b), + Sub => Some(a - b), + Mul => Some(a * b), + Div => Some(a / b), + Pow => Some(a.powf(b)), + } + } + + Expr::Func { name, args } => { + let name = name.as_str(); + match name { + "ln" => { + if args.len() != 1 { + return None; + } + Some(args[0].eval()?.ln()) + } + "log" => match args.len() { + 1 => Some(args[0].eval()?.log10()), + 2 => { + let base = args[0].eval()?; + let x = args[1].eval()?; + Some(x.log(base)) + } + _ => None, + }, + _ => None, + } + } } } - /// This parses an expression from a string (and is public) - /// - /// This function is public because it is used in the `handle_search_query_changed` function, - /// and the parse expression function, while doing the same thing, should not be public due to - /// the function name, not portraying the intention of the function. - pub fn from_str(s: &str) -> Option { - Self::parse_expression(s) + pub fn from_str(s: &str) -> Result { + let mut p = Parser::new(s); + let expr = p.parse_expr()?; + p.expect(Token::End)?; + Ok(expr) } +} + +/* ---------------- Tokenizer ---------------- */ + +#[derive(Debug, Clone, PartialEq)] +enum Token { + Number(f64), + Ident(String), + Plus, + Minus, + Star, + Slash, + Caret, + LParen, + RParen, + Comma, + End, +} - /// This is the function that parses an expression from a string - fn parse_expression(s: &str) -> Option { - let s = s.trim(); +struct Lexer<'a> { + input: &'a str, + i: usize, +} - // 1. Parse first (possibly signed) number with manual scan - let (first_str, rest) = Self::parse_signed_number_prefix(s)?; +impl<'a> Lexer<'a> { + fn new(input: &'a str) -> Self { + Self { input, i: 0 } + } - // 2. Next non‑whitespace char must be the binary operator - let rest = rest.trim_start(); - let (op_char, rest) = rest.chars().next().map(|c| (c, &rest[c.len_utf8()..]))?; + fn peek_char(&self) -> Option { + self.input[self.i..].chars().next() + } - let operation = match op_char { - '+' => Operation::Addition, - '-' => Operation::Subtraction, - '*' => Operation::Multiplication, - '/' => Operation::Division, - '^' => Operation::Power, - _ => return None, + fn bump_char(&mut self) -> Option { + let c = self.peek_char()?; + self.i += c.len_utf8(); + Some(c) + } + + fn skip_ws(&mut self) { + while matches!(self.peek_char(), Some(c) if c.is_whitespace()) { + self.bump_char(); + } + } + + fn next_token(&mut self) -> Result { + self.skip_ws(); + let c = match self.peek_char() { + Some(c) => c, + None => return Ok(Token::End), }; - // 3. The remainder should be the second (possibly signed) number - let rest = rest.trim_start(); - let (second_str, tail) = Self::parse_signed_number_prefix(rest)?; - // Optionally ensure nothing but whitespace after second number: - if !tail.trim().is_empty() { - return None; + // single-char tokens + let tok = match c { + '+' => { + self.bump_char(); + Token::Plus + } + '-' => { + self.bump_char(); + Token::Minus + } + '*' => { + self.bump_char(); + Token::Star + } + '/' => { + self.bump_char(); + Token::Slash + } + '^' => { + self.bump_char(); + Token::Caret + } + '(' => { + self.bump_char(); + Token::LParen + } + ')' => { + self.bump_char(); + Token::RParen + } + ',' => { + self.bump_char(); + Token::Comma + } + _ => { + // number or identifier + if c.is_ascii_digit() || c == '.' { + return self.lex_number(); + } else if c.is_ascii_alphabetic() || c == '_' { + return self.lex_ident(); + } else { + return Err(format!("Unexpected character: {c}")); + } + } + }; + Ok(tok) + } + + fn lex_number(&mut self) -> Result { + // Simple float lexer: digits/./e/E/+/- in exponent + let start = self.i; + let mut seen_e = false; + + while let Some(c) = self.peek_char() { + if c.is_ascii_digit() || c == '.' { + self.bump_char(); + continue; + } + if (c == 'e' || c == 'E') && !seen_e { + seen_e = true; + self.bump_char(); + // optional sign after exponent + if matches!(self.peek_char(), Some('+' | '-')) { + self.bump_char(); + } + continue; + } + break; } - let first_num: f64 = first_str.parse().ok()?; - let second_num: f64 = second_str.parse().ok()?; + let s = &self.input[start..self.i]; + let n = s + .parse::() + .map_err(|_| format!("Invalid number: {s}"))?; + Ok(Token::Number(n)) + } + + fn lex_ident(&mut self) -> Result { + let start = self.i; + while let Some(c) = self.peek_char() { + if c.is_ascii_alphanumeric() || c == '_' { + self.bump_char(); + } else { + break; + } + } + Ok(Token::Ident(self.input[start..self.i].to_string())) + } +} + +/* ---------------- Parser ---------------- */ + +struct Parser<'a> { + lex: Lexer<'a>, + cur: Token, +} + +impl<'a> Parser<'a> { + fn new(input: &'a str) -> Self { + let mut lex = Lexer::new(input); + let cur = lex.next_token().unwrap_or(Token::End); + Self { lex, cur } + } - Some(Expression { - first_num, - operation, - second_num, - }) + fn bump(&mut self) -> Result<(), String> { + self.cur = self.lex.next_token()?; + Ok(()) } - /// Returns (number_lexeme, remaining_slice) for a leading signed float. - /// Very simple: `[+|-]?` + "anything until we hit whitespace or an operator". - fn parse_signed_number_prefix(s: &str) -> Option<(&str, &str)> { - let s = s.trim_start(); - if s.is_empty() { - return None; + fn expect(&mut self, t: Token) -> Result<(), String> { + if self.cur == t { + self.bump() + } else { + Err(format!("Expected {:?}, found {:?}", t, self.cur)) } + } - let mut chars = s.char_indices().peekable(); + fn parse_expr(&mut self) -> Result { + // expr = term (('+'|'-') term)* + let mut node = self.parse_term()?; + loop { + let op = match self.cur { + Token::Plus => BinOp::Add, + Token::Minus => BinOp::Sub, + _ => break, + }; + self.bump()?; + let rhs = self.parse_term()?; + node = Expr::Binary { + op, + lhs: Box::new(node), + rhs: Box::new(rhs), + }; + } + Ok(node) + } - // Optional leading sign - if let Some((_, c)) = chars.peek() - && (*c == '+' || *c == '-') - { - chars.next(); + fn parse_term(&mut self) -> Result { + // term = power (('*'|'/') power)* + let mut node = self.parse_power()?; + loop { + let op = match self.cur { + Token::Star => BinOp::Mul, + Token::Slash => BinOp::Div, + _ => break, + }; + self.bump()?; + let rhs = self.parse_power()?; + node = Expr::Binary { + op, + lhs: Box::new(node), + rhs: Box::new(rhs), + }; } + Ok(node) + } - // Now consume until we hit an operator or whitespace - let mut end = 0; - while let Some((idx, c)) = chars.peek().cloned() { - if c.is_whitespace() || "+-*/^".contains(c) { - break; - } - end = idx + c.len_utf8(); - chars.next(); + fn parse_power(&mut self) -> Result { + // power = unary ('^' power)? (right associative) + let lhs = self.parse_unary()?; + if self.cur == Token::Caret { + self.bump()?; + let rhs = self.parse_power()?; + Ok(Expr::Binary { + op: BinOp::Pow, + lhs: Box::new(lhs), + rhs: Box::new(rhs), + }) + } else { + Ok(lhs) } + } - if end == 0 { - return None; // nothing that looks like a number + fn parse_unary(&mut self) -> Result { + // unary = ('+'|'-')* primary + match self.cur { + Token::Plus => { + self.bump()?; + Ok(Expr::Unary { + op: UnaryOp::Plus, + rhs: Box::new(self.parse_unary()?), + }) + } + Token::Minus => { + self.bump()?; + Ok(Expr::Unary { + op: UnaryOp::Minus, + rhs: Box::new(self.parse_unary()?), + }) + } + _ => self.parse_primary(), } + } - let (num, rest) = s.split_at(end); - Some((num, rest)) + fn parse_primary(&mut self) -> Result { + match &self.cur { + Token::Number(n) => { + let v = *n; + self.bump()?; + Ok(Expr::Number(v)) + } + Token::LParen => { + self.bump()?; + let e = self.parse_expr()?; + self.expect(Token::RParen)?; + Ok(e) + } + Token::Ident(name) => { + let name = name.clone(); + self.bump()?; + // function call must be ident '(' ... + self.expect(Token::LParen)?; + let mut args = Vec::new(); + if self.cur != Token::RParen { + loop { + args.push(self.parse_expr()?); + if self.cur == Token::Comma { + self.bump()?; + continue; + } + break; + } + } + self.expect(Token::RParen)?; + Ok(Expr::Func { name, args }) + } + _ => Err(format!("Unexpected token: {:?}", self.cur)), + } } } diff --git a/src/commands.rs b/src/commands.rs index 536969c..bfffee1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,7 @@ use arboard::Clipboard; use objc2_app_kit::NSWorkspace; use objc2_foundation::NSURL; -use crate::{calculator::Expression, clipboard::ClipBoardContentType, config::Config}; +use crate::{calculator::Expr, clipboard::ClipBoardContentType, config::Config}; /// The different functions that rustcast can perform #[derive(Debug, Clone, PartialEq)] @@ -17,7 +17,7 @@ pub enum Function { RandomVar(i32), // Easter egg function CopyToClipboard(ClipBoardContentType), GoogleSearch(String), - Calculate(Expression), + Calculate(Expr), OpenPrefPane, Quit, } @@ -86,7 +86,7 @@ impl Function { Function::Calculate(expr) => { Clipboard::new() .unwrap() - .set_text(expr.eval().to_string()) + .set_text(expr.eval().map(|x| x.to_string()).unwrap_or("".to_string())) .unwrap_or(()); }