188 lines
5.6 KiB
Rust
188 lines
5.6 KiB
Rust
//! Math calculation API for Lua plugins
|
|
//!
|
|
//! Provides safe math expression evaluation:
|
|
//! - `owlry.math.calculate(expression)` - Evaluate a math expression
|
|
|
|
use mlua::{Lua, Result as LuaResult, Table};
|
|
|
|
/// Register math APIs
|
|
pub fn register_math_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
|
let math_table = lua.create_table()?;
|
|
|
|
// owlry.math.calculate(expression) -> number or nil, error
|
|
// Evaluates a mathematical expression safely
|
|
// Returns (result, nil) on success or (nil, error_message) on failure
|
|
math_table.set(
|
|
"calculate",
|
|
lua.create_function(
|
|
|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
|
|
match meval::eval_str(&expr) {
|
|
Ok(result) => {
|
|
if result.is_finite() {
|
|
Ok((Some(result), None))
|
|
} else {
|
|
Ok((None, Some("Result is not a finite number".to_string())))
|
|
}
|
|
}
|
|
Err(e) => Ok((None, Some(e.to_string()))),
|
|
}
|
|
},
|
|
)?,
|
|
)?;
|
|
|
|
// owlry.math.calc(expression) -> number (throws on error)
|
|
// Convenience function that throws instead of returning error
|
|
math_table.set(
|
|
"calc",
|
|
lua.create_function(|_lua, expr: String| {
|
|
meval::eval_str(&expr)
|
|
.map_err(|e| mlua::Error::external(format!("Math error: {}", e)))
|
|
.and_then(|r| {
|
|
if r.is_finite() {
|
|
Ok(r)
|
|
} else {
|
|
Err(mlua::Error::external("Result is not a finite number"))
|
|
}
|
|
})
|
|
})?,
|
|
)?;
|
|
|
|
// owlry.math.is_expression(str) -> boolean
|
|
// Check if a string looks like a math expression
|
|
math_table.set(
|
|
"is_expression",
|
|
lua.create_function(|_lua, expr: String| {
|
|
let trimmed = expr.trim();
|
|
|
|
// Must have at least one digit
|
|
if !trimmed.chars().any(|c| c.is_ascii_digit()) {
|
|
return Ok(false);
|
|
}
|
|
|
|
// Should only contain valid math characters
|
|
let valid = trimmed.chars().all(|c| {
|
|
c.is_ascii_digit()
|
|
|| c.is_ascii_alphabetic()
|
|
|| matches!(c, '+' | '-' | '*' | '/' | '^' | '(' | ')' | '.' | ' ' | '%')
|
|
});
|
|
|
|
Ok(valid)
|
|
})?,
|
|
)?;
|
|
|
|
// owlry.math.format(number, decimals?) -> string
|
|
// Format a number with optional decimal places
|
|
math_table.set(
|
|
"format",
|
|
lua.create_function(|_lua, (num, decimals): (f64, Option<usize>)| {
|
|
let decimals = decimals.unwrap_or(2);
|
|
|
|
// Check if it's effectively an integer
|
|
if (num - num.round()).abs() < f64::EPSILON {
|
|
Ok(format!("{}", num as i64))
|
|
} else {
|
|
Ok(format!("{:.prec$}", num, prec = decimals))
|
|
}
|
|
})?,
|
|
)?;
|
|
|
|
owlry.set("math", math_table)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn setup_lua() -> Lua {
|
|
let lua = Lua::new();
|
|
let owlry = lua.create_table().unwrap();
|
|
register_math_api(&lua, &owlry).unwrap();
|
|
lua.globals().set("owlry", owlry).unwrap();
|
|
lua
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_basic() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(
|
|
r#"
|
|
local result, err = owlry.math.calculate("2 + 2")
|
|
if err then error(err) end
|
|
return result
|
|
"#,
|
|
);
|
|
let result: f64 = chunk.call(()).unwrap();
|
|
assert!((result - 4.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_complex() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(
|
|
r#"
|
|
local result, err = owlry.math.calculate("sqrt(16) + 2^3")
|
|
if err then error(err) end
|
|
return result
|
|
"#,
|
|
);
|
|
let result: f64 = chunk.call(()).unwrap();
|
|
assert!((result - 12.0).abs() < f64::EPSILON); // sqrt(16) = 4, 2^3 = 8
|
|
}
|
|
|
|
#[test]
|
|
fn test_calculate_error() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(
|
|
r#"
|
|
local result, err = owlry.math.calculate("invalid expression @@")
|
|
if result then
|
|
return false -- should not succeed
|
|
else
|
|
return true -- correctly failed
|
|
end
|
|
"#,
|
|
);
|
|
let had_error: bool = chunk.call(()).unwrap();
|
|
assert!(had_error);
|
|
}
|
|
|
|
#[test]
|
|
fn test_calc_throws() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(r#"return owlry.math.calc("3 * 4")"#);
|
|
let result: f64 = chunk.call(()).unwrap();
|
|
assert!((result - 12.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_expression() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(r#"return owlry.math.is_expression("2 + 2")"#);
|
|
let is_expr: bool = chunk.call(()).unwrap();
|
|
assert!(is_expr);
|
|
|
|
let chunk = lua.load(r#"return owlry.math.is_expression("hello world")"#);
|
|
let is_expr: bool = chunk.call(()).unwrap();
|
|
assert!(!is_expr);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format() {
|
|
let lua = setup_lua();
|
|
|
|
let chunk = lua.load(r#"return owlry.math.format(3.14159, 2)"#);
|
|
let formatted: String = chunk.call(()).unwrap();
|
|
assert_eq!(formatted, "3.14");
|
|
|
|
let chunk = lua.load(r#"return owlry.math.format(42.0)"#);
|
|
let formatted: String = chunk.call(()).unwrap();
|
|
assert_eq!(formatted, "42");
|
|
}
|
|
}
|