From b3fe0636bcec81138179c603876b028864be58b2 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Tue, 27 May 2025 19:47:24 +0200 Subject: [PATCH] improve math, fixes #69 --- Cargo.lock | 7 -- worf/Cargo.toml | 1 - worf/src/lib/modes/math.rs | 201 ++++++++++++++++++++++++++++++++++--- 3 files changed, 188 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8a313f..679025e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,12 +675,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fasteval3" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ebd4dfc97a204b81366c95809cddaf586b713c7b3dc7ccec2e0c9c2bd5a62c0" - [[package]] name = "fastrand" version = "2.3.0" @@ -2881,7 +2875,6 @@ dependencies = [ "dirs 6.0.0", "emoji", "env_logger", - "fasteval3", "freedesktop-file-parser", "freedesktop-icons", "gdk4", diff --git a/worf/Cargo.toml b/worf/Cargo.toml index 029278a..81d1e09 100644 --- a/worf/Cargo.toml +++ b/worf/Cargo.toml @@ -46,7 +46,6 @@ freedesktop-file-parser = { git = "https://github.com/alexmohr/desktop_file_pars strsim = "0.11.1" dirs = "6.0.0" which = "7.0.3" -fasteval3 = "3.0.1" tree_magic_mini = "3.1.6" rayon = "1.10.0" nix = { version = "0.30.0", features = ["process"] } diff --git a/worf/src/lib/modes/math.rs b/worf/src/lib/modes/math.rs index 7e8e92b..956ad4f 100644 --- a/worf/src/lib/modes/math.rs +++ b/worf/src/lib/modes/math.rs @@ -1,4 +1,5 @@ use regex::Regex; +use std::collections::VecDeque; use crate::{ config::Config, @@ -27,19 +28,7 @@ impl ItemProvider for MathProvider { #[allow(clippy::cast_possible_truncation)] fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec>) { if let Some(search_text) = search { - let re = Regex::new(r"0x[0-9a-fA-F]+").unwrap(); - let result = re.replace_all(search_text, |caps: ®ex::Captures| { - let hex_str = &caps[0][2..]; // Skip "0x" - let decimal = u64::from_str_radix(hex_str, 16).unwrap(); - decimal.to_string() - }); - - // todo maybe we want to support variables later? - let mut ns = fasteval3::EmptyNamespace; - let result = match fasteval3::ez_eval(&result, &mut ns) { - Ok(result) => format!("{} (0x{:X})", result, result as i64), - Err(e) => format!("failed to calculate {e:?}"), - }; + let result = calc(search_text); let item = MenuItem::new( result, @@ -63,6 +52,192 @@ impl ItemProvider for MathProvider { } } +#[derive(Debug, Clone, Copy)] +enum Token { + Num(i64), + Op(char), + ShiftLeft, + ShiftRight, + Power, +} + +enum Value { + Int(i64), + Float(f64), +} + +/// Normalize base literals like 0x and 0b into decimal format +fn normalize_bases(expr: &str) -> String { + let hex_re = Regex::new(r"0x[0-9a-fA-F]+").unwrap(); + let expr = hex_re.replace_all(expr, |caps: ®ex::Captures| { + i64::from_str_radix(&caps[0][2..], 16).unwrap().to_string() + }); + + let bin_re = Regex::new(r"0b[01]+").unwrap(); + bin_re + .replace_all(&expr, |caps: ®ex::Captures| { + i64::from_str_radix(&caps[0][2..], 2).unwrap().to_string() + }) + .to_string() +} + +/// Tokenize a normalized expression string into tokens +fn tokenize(expr: &str) -> Result, String> { + let mut tokens = VecDeque::new(); + let chars: Vec = expr.chars().collect(); + let mut i = 0; + + while i < chars.len() { + let c = chars[i]; + + if c.is_whitespace() { + i += 1; + continue; + } + + // Multi-character operators + if i + 1 < chars.len() { + match &expr[i..=i + 1] { + "<<" => { + tokens.push_back(Token::ShiftLeft); + i += 2; + continue; + } + ">>" => { + tokens.push_back(Token::ShiftRight); + i += 2; + continue; + } + "**" => { + tokens.push_back(Token::Power); + i += 2; + continue; + } + _ => {} + } + } + + // Single-character operators or digits + match c { + '+' | '-' | '*' | '/' | '&' | '|' | '^' => { + tokens.push_back(Token::Op(c)); + i += 1; + } + '0'..='9' => { + let start = i; + while i < chars.len() && chars[i].is_ascii_digit() { + i += 1; + } + let num_str: String = chars[start..i].iter().collect(); + let n = num_str.parse::().unwrap(); + tokens.push_back(Token::Num(n)); + } + _ => return Err("Invalid character in expression".to_owned()), + } + } + + Ok(tokens) +} + +fn to_f64(v: &Value) -> f64 { + match v { + #[allow(clippy::cast_precision_loss)] + Value::Int(i) => *i as f64, + Value::Float(f) => *f, + } +} + +fn to_i64(v: &Value) -> i64 { + match v { + Value::Int(i) => *i, + #[allow(clippy::cast_possible_truncation)] + Value::Float(f) => *f as i64, + } +} + +/// Apply an operator to two values +fn apply_op(a: &Value, b: &Value, op: &Token) -> Value { + match op { + Token::Op('+') => Value::Float(to_f64(a) + to_f64(b)), + Token::Op('-') => Value::Float(to_f64(a) - to_f64(b)), + Token::Op('*') => Value::Float(to_f64(a) * to_f64(b)), + Token::Op('/') => Value::Float(to_f64(a) / to_f64(b)), + Token::Power => Value::Float(to_f64(a).powf(to_f64(b))), + Token::Op('&') => Value::Int(to_i64(a) & to_i64(b)), + Token::Op('|') => Value::Int(to_i64(a) | to_i64(b)), + Token::Op('^') => Value::Int(to_i64(a) ^ to_i64(b)), + Token::ShiftLeft => Value::Int(to_i64(a) << to_i64(b)), + Token::ShiftRight => Value::Int(to_i64(a) >> to_i64(b)), + _ => panic!("Unknown operator"), + } +} + +/// Return precedence of operator (lower number = higher precedence) +fn precedence(op: &Token) -> u8 { + match op { + Token::Power => 1, + Token::ShiftLeft | Token::ShiftRight => 2, + Token::Op('*' | '/') => 3, + Token::Op('+' | '-') => 4, + Token::Op('&') => 5, + Token::Op('^') => 6, + Token::Op('|') => 7, + _ => 100, + } +} + +/// Evaluate the tokenized expression using shunting yard algorithm +fn eval_expr(tokens: &mut VecDeque) -> Result { + let mut values = Vec::new(); + let mut ops = Vec::new(); + + while let Some(token) = tokens.pop_front() { + match token { + Token::Num(n) => values.push(Value::Int(n)), + op @ (Token::Op(_) | Token::ShiftLeft | Token::ShiftRight | Token::Power) => { + while let Some(top_op) = ops.last() { + if precedence(&op) >= precedence(top_op) { + let b = values.pop().ok_or("Missing left operand")?; + let a = values.pop().ok_or("Missing right operand")?; + let op = ops.pop().ok_or("Missing operator")?; + values.push(apply_op(&a, &b, &op)); + } else { + break; + } + } + ops.push(op); + } + } + } + + while let Some(op) = ops.pop() { + let b = values + .pop() + .ok_or("Missing right operand in final evaluation")?; + let a = values + .pop() + .ok_or("Missing left operand in final evaluation")?; + values.push(apply_op(&a, &b, &op)); + } + + values.pop().ok_or("No result after evaluation".to_owned()) +} + +/// Entry point: takes raw input, normalizes and evaluates it +fn calc(input: &str) -> String { + let normalized = normalize_bases(input); + let mut tokens = match tokenize(&normalized) { + Ok(t) => t, + Err(e) => return e, + }; + + match eval_expr(&mut tokens) { + Ok(Value::Int(i)) => format!("{i} (0x{i:X})"), + Ok(Value::Float(f)) => format!("{f}"), + Err(e) => e, + } +} + /// Shows the math mode pub fn show(config: &Config) { let mut calc: Vec> = vec![];