improve math, fixes #69
This commit is contained in:
parent
fbe5454c7b
commit
b3fe0636bc
3 changed files with 188 additions and 21 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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: ®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<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: ®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<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![];
|
||||
|
|
Loading…
Add table
Reference in a new issue