improve math, fixes #69

This commit is contained in:
Alexander Mohr 2025-05-27 19:47:24 +02:00
parent fbe5454c7b
commit b3fe0636bc
3 changed files with 188 additions and 21 deletions

7
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -1,4 +1,5 @@
use regex::Regex;
use std::collections::VecDeque;
use crate::{
config::Config,
@ -27,19 +28,7 @@ impl<T: Clone> ItemProvider<T> for MathProvider<T> {
#[allow(clippy::cast_possible_truncation)]
fn get_elements(&mut self, search: Option<&str>) -> (bool, Vec<MenuItem<T>>) {
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: &regex::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<T: Clone> ItemProvider<T> for MathProvider<T> {
}
}
#[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: &regex::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: &regex::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<VecDeque<Token>, String> {
let mut tokens = VecDeque::new();
let chars: Vec<char> = 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::<i64>().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<Token>) -> Result<Value, String> {
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<MenuItem<String>> = vec![];