use std::f64::consts::{E, PI};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExprError {
message: String,
}
impl ExprError {
fn new(message: String) -> Self {
ExprError { message }
}
pub(crate) fn message(&self) -> &str {
&self.message
}
}
const MAX_STACK_SIZE: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Negative,
Positive,
Not,
Times,
Divide,
Mod,
Fmod,
Plus,
Minus,
Round,
Equality,
Less,
Greater,
LessEq,
GreaterEq,
NotEq,
And,
Or,
Exponent,
Sine,
Cosine,
Tangens,
Arcsine,
Arccos,
Arctan,
Ln,
Exp,
Abs,
Floor,
Trunc,
Ceil,
Pow,
Pi,
Sqrt,
Open,
Close,
}
fn precedence(op: Op) -> i32 {
match op {
Op::Negative | Op::Positive | Op::Exponent => 10,
Op::Sine
| Op::Cosine
| Op::Tangens
| Op::Arcsine
| Op::Arccos
| Op::Arctan
| Op::Exp
| Op::Ln
| Op::Abs
| Op::Floor
| Op::Trunc
| Op::Ceil
| Op::Not
| Op::Sqrt => 9,
Op::Pow => 8,
Op::Times | Op::Divide | Op::Mod | Op::Fmod => 7,
Op::Plus | Op::Minus => 6,
Op::Round => 5,
Op::Equality | Op::Less | Op::Greater | Op::LessEq | Op::GreaterEq | Op::NotEq => 4,
Op::And => 3,
Op::Or => 2,
Op::Pi => 0,
Op::Open | Op::Close => -1,
}
}
fn op_name(op: Op) -> &'static str {
match op {
Op::Negative | Op::Minus => "-",
Op::Positive | Op::Plus => "+",
Op::Not => "not",
Op::Times => "*",
Op::Divide => "/",
Op::Mod => "mod",
Op::Fmod => "fmod",
Op::Round => "round",
Op::Equality => "=",
Op::Less => "<",
Op::Greater => ">",
Op::LessEq => "<=",
Op::GreaterEq => ">=",
Op::NotEq => "<>",
Op::And => "and",
Op::Or => "or",
Op::Exponent => "e",
Op::Sine => "sin",
Op::Cosine => "cos",
Op::Tangens => "tan",
Op::Arcsine => "asin",
Op::Arccos => "acos",
Op::Arctan => "atan",
Op::Ln => "ln",
Op::Exp => "exp",
Op::Abs => "abs",
Op::Floor => "floor",
Op::Trunc => "trunc",
Op::Ceil => "ceil",
Op::Pow => "^",
Op::Pi => "pi",
Op::Sqrt => "sqrt",
Op::Open | Op::Close => "(",
}
}
fn is_white(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\r' | b'\n')
}
fn is_number_byte(b: u8) -> bool {
b.is_ascii_digit() || b == b'.'
}
fn word_to_op(word: &str) -> Option<Op> {
let op = match word {
"mod" => Op::Mod,
"fmod" => Op::Fmod,
"and" => Op::And,
"or" => Op::Or,
"not" => Op::Not,
"round" => Op::Round,
"div" => Op::Divide,
"e" => Op::Exponent,
"sin" => Op::Sine,
"cos" => Op::Cosine,
"tan" => Op::Tangens,
"asin" => Op::Arcsine,
"acos" => Op::Arccos,
"atan" => Op::Arctan,
"exp" => Op::Exp,
"ln" => Op::Ln,
"abs" => Op::Abs,
"trunc" => Op::Trunc,
"floor" => Op::Floor,
"ceil" => Op::Ceil,
"pi" => Op::Pi,
"sqrt" => Op::Sqrt,
_ => return None,
};
Some(op)
}
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) fn eval(input: &str) -> Result<String, ExprError> {
let expr = input
.replace("<", "<")
.replace(">", ">")
.replace("−", "-")
.replace('\u{2212}', "-");
let bytes = expr.as_bytes();
let end = bytes.len();
let mut operands: Vec<f64> = Vec::new();
let mut operators: Vec<Op> = Vec::new();
let mut expecting_expression = true;
let mut p = 0usize;
while p < end {
if operands.len() > MAX_STACK_SIZE || operators.len() > MAX_STACK_SIZE {
return Err(ExprError::new(
"Expression error: Stack exhausted.".to_owned(),
));
}
let b = bytes[p];
let b1 = if p + 1 < end { bytes[p + 1] } else { 0 };
let binary: Option<(Op, String)>;
if is_white(b) {
while p < end && is_white(bytes[p]) {
p += 1;
}
continue;
} else if is_number_byte(b) {
if !expecting_expression {
return Err(ExprError::new(
"Expression error: Unexpected number.".to_owned(),
));
}
let start = p;
while p < end && is_number_byte(bytes[p]) {
p += 1;
}
operands.push(parse_php_float(&expr[start..p]));
expecting_expression = false;
continue;
} else if b.is_ascii_alphabetic() {
let start = p;
while p < end && bytes[p].is_ascii_alphabetic() {
p += 1;
}
let word = expr[start..p].to_ascii_lowercase();
let Some(op) = word_to_op(&word) else {
return Err(ExprError::new(format!(
"Expression error: Unrecognized word \"{word}\"."
)));
};
match op {
Op::Exponent => {
if expecting_expression {
operands.push(E);
expecting_expression = false;
continue;
}
binary = Some((Op::Exponent, word));
}
Op::Pi => {
if !expecting_expression {
return Err(ExprError::new(
"Expression error: Unexpected number.".to_owned(),
));
}
operands.push(PI);
expecting_expression = false;
continue;
}
Op::Not
| Op::Sine
| Op::Cosine
| Op::Tangens
| Op::Arcsine
| Op::Arccos
| Op::Arctan
| Op::Exp
| Op::Ln
| Op::Abs
| Op::Floor
| Op::Trunc
| Op::Ceil
| Op::Sqrt => {
if !expecting_expression {
return Err(ExprError::new(format!(
"Expression error: Unexpected {word} operator."
)));
}
operators.push(op);
continue;
}
_ => binary = Some((op, word)),
}
} else if b == b'<' && b1 == b'=' {
p += 2;
binary = Some((Op::LessEq, "<=".to_owned()));
} else if b == b'>' && b1 == b'=' {
p += 2;
binary = Some((Op::GreaterEq, ">=".to_owned()));
} else if (b == b'<' && b1 == b'>') || (b == b'!' && b1 == b'=') {
let name = if b == b'!' { "!=" } else { "<>" };
p += 2;
binary = Some((Op::NotEq, name.to_owned()));
} else if b == b'+' {
p += 1;
if expecting_expression {
operators.push(Op::Positive);
continue;
}
binary = Some((Op::Plus, "+".to_owned()));
} else if b == b'-' {
p += 1;
if expecting_expression {
operators.push(Op::Negative);
continue;
}
binary = Some((Op::Minus, "-".to_owned()));
} else if b == b'*' {
p += 1;
binary = Some((Op::Times, "*".to_owned()));
} else if b == b'/' {
p += 1;
binary = Some((Op::Divide, "/".to_owned()));
} else if b == b'^' {
p += 1;
binary = Some((Op::Pow, "^".to_owned()));
} else if b == b'(' {
if !expecting_expression {
return Err(ExprError::new(
"Expression error: Unexpected ( operator.".to_owned(),
));
}
operators.push(Op::Open);
p += 1;
continue;
} else if b == b')' {
loop {
match operators.last().copied() {
Some(Op::Open) => {
operators.pop();
break;
}
Some(top) => {
operators.pop();
do_operation(top, &mut operands)?;
}
None => {
return Err(ExprError::new(
"Expression error: Unexpected closing bracket.".to_owned(),
));
}
}
}
expecting_expression = false;
p += 1;
continue;
} else if b == b'=' {
p += 1;
binary = Some((Op::Equality, "=".to_owned()));
} else if b == b'<' {
p += 1;
binary = Some((Op::Less, "<".to_owned()));
} else if b == b'>' {
p += 1;
binary = Some((Op::Greater, ">".to_owned()));
} else {
let ch = expr.get(p..).and_then(|s| s.chars().next()).unwrap_or('?');
return Err(ExprError::new(format!(
"Expression error: Unrecognized punctuation character \"{ch}\"."
)));
}
if let Some((op, name)) = binary {
if expecting_expression {
return Err(ExprError::new(format!(
"Expression error: Unexpected {name} operator."
)));
}
while let Some(top) = operators.last().copied() {
if precedence(op) <= precedence(top) {
operators.pop();
do_operation(top, &mut operands)?;
} else {
break;
}
}
operators.push(op);
expecting_expression = true;
}
}
while let Some(op) = operators.pop() {
if op == Op::Open {
return Err(ExprError::new(
"Expression error: Unclosed bracket.".to_owned(),
));
}
do_operation(op, &mut operands)?;
}
Ok(operands
.iter()
.map(|v| format_double(*v))
.collect::<Vec<_>>()
.join("<br />\n"))
}
fn do_operation(op: Op, stack: &mut Vec<f64>) -> Result<(), ExprError> {
fn missing(op: Op) -> ExprError {
ExprError::new(format!(
"Expression error: Missing operand for {}.",
op_name(op)
))
}
fn div_zero() -> ExprError {
ExprError::new("Division by zero.".to_owned())
}
match op {
Op::Negative => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(-a);
}
Op::Positive => {
if stack.is_empty() {
return Err(missing(op));
}
}
Op::Times => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(l * r);
}
Op::Divide => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
if r == 0.0 {
return Err(div_zero());
}
stack.push(l / r);
}
Op::Mod => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0) as i64;
let l = stack.pop().unwrap_or(0.0) as i64;
if r == 0 {
return Err(div_zero());
}
stack.push((l % r) as f64);
}
Op::Fmod => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
if r == 0.0 {
return Err(div_zero());
}
stack.push(l % r);
}
Op::Plus => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(l + r);
}
Op::Minus => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(l - r);
}
Op::And => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l != 0.0 && r != 0.0 { 1.0 } else { 0.0 });
}
Op::Or => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l != 0.0 || r != 0.0 { 1.0 } else { 0.0 });
}
Op::Equality => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l == r { 1.0 } else { 0.0 });
}
Op::Not => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(if a == 0.0 { 1.0 } else { 0.0 });
}
Op::Round => {
if stack.len() < 2 {
return Err(missing(op));
}
let digits = stack.pop().unwrap_or(0.0) as i32;
let value = stack.pop().unwrap_or(0.0);
stack.push(php_round(value, digits));
}
Op::Less => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l < r { 1.0 } else { 0.0 });
}
Op::Greater => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l > r { 1.0 } else { 0.0 });
}
Op::LessEq => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l <= r { 1.0 } else { 0.0 });
}
Op::GreaterEq => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l >= r { 1.0 } else { 0.0 });
}
Op::NotEq => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(if l != r { 1.0 } else { 0.0 });
}
Op::Exponent => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(l * 10f64.powf(r));
}
Op::Sine => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.sin());
}
Op::Cosine => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.cos());
}
Op::Tangens => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.tan());
}
Op::Arcsine => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
if !(-1.0..=1.0).contains(&a) {
return Err(ExprError::new(format!(
"Invalid argument for {}: less than -1 or greater than 1.",
op_name(op)
)));
}
stack.push(a.asin());
}
Op::Arccos => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
if !(-1.0..=1.0).contains(&a) {
return Err(ExprError::new(format!(
"Invalid argument for {}: less than -1 or greater than 1.",
op_name(op)
)));
}
stack.push(a.acos());
}
Op::Arctan => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.atan());
}
Op::Exp => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.exp());
}
Op::Ln => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
if a <= 0.0 {
return Err(ExprError::new(
"Invalid argument for ln: less than or equal to 0.".to_owned(),
));
}
stack.push(a.ln());
}
Op::Abs => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.abs());
}
Op::Floor => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.floor());
}
Op::Trunc => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.trunc());
}
Op::Ceil => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
stack.push(a.ceil());
}
Op::Pow => {
if stack.len() < 2 {
return Err(missing(op));
}
let r = stack.pop().unwrap_or(0.0);
let l = stack.pop().unwrap_or(0.0);
stack.push(l.powf(r));
}
Op::Sqrt => {
if stack.is_empty() {
return Err(missing(op));
}
let a = stack.pop().unwrap_or(0.0);
let result = a.sqrt();
if result.is_nan() {
return Err(ExprError::new(format!(
"In {}: Result is not a number.",
op_name(op)
)));
}
stack.push(result);
}
Op::Pi | Op::Open | Op::Close => {
return Err(ExprError::new(
"Expression error: Unknown error ().".to_owned(),
));
}
}
Ok(())
}
fn parse_php_float(token: &str) -> f64 {
let mut prefix = String::new();
let mut seen_dot = false;
for c in token.chars() {
if c == '.' {
if seen_dot {
break;
}
seen_dot = true;
}
prefix.push(c);
}
if prefix.is_empty() || prefix == "." {
return 0.0;
}
if prefix.starts_with('.') {
prefix.insert(0, '0');
}
if prefix.ends_with('.') {
prefix.push('0');
}
prefix.parse::<f64>().unwrap_or(0.0)
}
fn format_double(v: f64) -> String {
if v.is_nan() {
return "NAN".to_owned();
}
if v.is_infinite() {
return if v < 0.0 { "-INF" } else { "INF" }.to_owned();
}
let sign = v.is_sign_negative();
let (digits, decpt) = dtoa14(v.abs());
let ndigit: i32 = 14;
let mut out = String::new();
if sign {
out.push('-');
}
if (decpt >= 0 && decpt > ndigit) || decpt < -3 {
let mut e = decpt - 1;
let neg_exp = e < 0;
if neg_exp {
e = -e;
}
let mut it = digits.chars();
out.push(it.next().unwrap_or('0'));
out.push('.');
let rest: String = it.collect();
if rest.is_empty() {
out.push('0');
} else {
out.push_str(&rest);
}
out.push('E');
out.push(if neg_exp { '-' } else { '+' });
out.push_str(&e.to_string());
} else if decpt < 0 {
out.push('0');
out.push('.');
for _ in 0..(-decpt) {
out.push('0');
}
out.push_str(&digits);
} else {
let d: Vec<char> = digits.chars().collect();
let dp = decpt as usize;
let mut i = 0usize;
while i < dp {
out.push(if i < d.len() { d[i] } else { '0' });
i += 1;
}
if dp < d.len() {
if dp == 0 {
out.push('0');
}
out.push('.');
for &ch in &d[dp..] {
out.push(ch);
}
}
}
out
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn dtoa14(value: f64) -> (String, i32) {
if value == 0.0 {
return ("0".to_owned(), 1);
}
let s = format!("{value:.13e}");
let (mantissa, exp) = s.split_once('e').unwrap_or((s.as_str(), "0"));
let exp: i32 = exp.parse().unwrap_or(0);
let all_digits: String = mantissa.chars().filter(|c| *c != '.').collect();
let decpt = exp + 1;
let trimmed = all_digits.trim_end_matches('0');
let digits = if trimmed.is_empty() {
"0".to_owned()
} else {
trimmed.to_owned()
};
(digits, decpt)
}
fn intpow10(power: i32) -> f64 {
const P: [f64; 23] = [
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
];
if (0..=22).contains(&power) {
P[power as usize]
} else {
10f64.powi(power)
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn intlog10abs(value: f64) -> i32 {
let value = value.abs();
if value == 0.0 {
return 0;
}
let mut e = value.log10().floor() as i32;
while intpow10(e) > value {
e -= 1;
}
while intpow10(e + 1) <= value {
e += 1;
}
e
}
fn php_round(value: f64, places: i32) -> f64 {
if !value.is_finite() || value == 0.0 {
return value;
}
let f1 = intpow10(places.abs());
let precision_places = 14 - intlog10abs(value);
let tmp_value;
if precision_places > places && precision_places - 15 < places {
let mut use_precision = precision_places.max(-60);
let f2 = intpow10(use_precision.abs());
let mut t = if use_precision >= 0 {
value * f2
} else {
value / f2
};
t = round_helper(t);
use_precision = (places - use_precision).clamp(-60, 60);
let f2b = intpow10(use_precision.abs());
t = if use_precision >= 0 { t * f2b } else { t / f2b };
tmp_value = round_helper(t);
} else {
let t = if places >= 0 { value * f1 } else { value / f1 };
if t.abs() >= 1e15 {
return value;
}
tmp_value = round_helper(t);
}
if places.abs() < 23 {
if places > 0 {
tmp_value / f1
} else {
tmp_value * f1
}
} else {
let reparsed = format!("{tmp_value:.6}e{}", -places).parse::<f64>();
match reparsed {
Ok(x) if x.is_finite() && x != 0.0 => x,
_ => value,
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn round_helper(value: f64) -> f64 {
if value >= 0.0 {
let mut t = (value + 0.5).floor();
if t - value > 0.5 {
t -= 1.0;
} else if t - value <= -0.5 {
t += 1.0;
}
t
} else {
let mut t = (value - 0.5).ceil();
if t - value < -0.5 {
t += 1.0;
} else if t - value >= 0.5 {
t -= 1.0;
}
t
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(s: &str) -> String {
eval(s).unwrap()
}
fn err(s: &str) -> String {
eval(s).unwrap_err().message().to_owned()
}
#[test]
fn empty_expression_is_empty() {
assert_eq!(eval(" "), Ok(String::new()));
assert_eq!(eval(""), Ok(String::new()));
}
#[test]
fn reference_test_suite_values() {
let cases: &[(&str, &str)] = &[
("1 or 0", "1"),
("not (1 and 0)", "1"),
("not 0", "1"),
("4 < 5", "1"),
("-5 < 2", "1"),
("-2 <= -2", "1"),
("4 > 3", "1"),
("4 > -3", "1"),
("5 >= 2", "1"),
("2 >= 2", "1"),
("1 != 2", "1"),
("-4 * -4 = 4 * 4", "1"),
("not (1 != 1)", "1"),
("1 + 1", "2"),
("-1 + 1", "0"),
("+1 + 1", "2"),
("4 * 4", "16"),
("(1/3) * 3", "1"),
("3 / 1.5", "2"),
("3 / 0.2", "15"),
("3 / ( 2.0 * 0.1 )", "15"),
("3 / ( 2.0 / 10 )", "15"),
("3 / (- 0.2 )", "-15"),
("3 / abs( 0.2 )", "15"),
("3 mod 2", "1"),
("1e4", "10000"),
("1e-2", "0.01"),
("4.0 round 0", "4"),
("ceil 4", "4"),
("floor 4", "4"),
("4.5 round 0", "5"),
("4.2 round 0", "4"),
("-4.2 round 0", "-4"),
("-4.5 round 0", "-5"),
("-2.0 round 0", "-2"),
("ceil -3", "-3"),
("floor -6.0", "-6"),
("ceil 4.2", "5"),
("ceil -4.5", "-4"),
("floor -4.5", "-5"),
("abs(-2)", "2"),
("ln(exp(1))", "1"),
("trunc(4.5)", "4"),
("trunc(-4.5)", "-4"),
("123 fmod (2^64-1)", "123"),
("5.7 mod 1.3", "0"),
("5.7 fmod 1.3", "0.5"),
("pi + 1", "4.1415926535898"),
("sin(0)", "0"),
("cos(0)", "1"),
("tan(0)", "0"),
("asin(0)", "0"),
("acos(1)", "0"),
("atan(0)", "0"),
("sqrt(4)", "2"),
];
for (input, expected) in cases {
assert_eq!(ev(input), *expected, "input: {input}");
}
}
#[test]
fn values_verified_against_live_api() {
let cases: &[(&str, &str)] = &[
("10/3", "3.3333333333333"),
("2^10", "1024"),
("7 mod 3", "1"),
("-7 mod 3", "-1"),
("7 mod -3", "1"),
("2.5 round 0", "3"),
("8.5 round 0", "9"),
("5.5 round 0", "6"),
("0.15 round 1", "0.2"),
("-0.15 round 1", "-0.2"),
("2.675 round 2", "2.68"),
("1234.5678 round -2", "1200"),
("1234.5678 round 2", "1234.57"),
("-2^2", "4"),
("2^-2", "0.25"),
("2^0.5", "1.4142135623731"),
("1/3", "0.33333333333333"),
("1000000*1000000", "1000000000000"),
("sin(1)", "0.8414709848079"),
("ln(2)", "0.69314718055995"),
("0.1+0.2", "0.3"),
("1e20", "1.0E+20"),
("1e15", "1.0E+15"),
("1e14", "1.0E+14"),
("1e13", "10000000000000"),
("1e-4", "0.0001"),
("1e-5", "1.0E-5"),
("0*-1", "-0"),
("1.5e300*1e300", "INF"),
];
for (input, expected) in cases {
assert_eq!(ev(input), *expected, "input: {input}");
}
}
#[test]
fn power_is_left_associative() {
assert_eq!(ev("2^3^2"), "64");
}
#[test]
fn euler_constant_and_exponent_operator() {
assert_eq!(ev("e = exp(1)"), "1");
assert_eq!(ev("2 e 3"), "2000");
}
#[test]
fn logical_operators() {
assert_eq!(ev("0 and 1"), "0");
assert_eq!(ev("0 or 0"), "0");
assert_eq!(ev("5 and 3"), "1");
assert_eq!(ev("not 5"), "0");
assert_eq!(ev("1 = 1"), "1");
assert_eq!(ev("1 <> 2"), "1");
assert_eq!(ev("1 != 1"), "0");
assert_eq!(ev("2 <= 1"), "0");
assert_eq!(ev("2 >= 3"), "0");
}
#[test]
fn unary_and_div_mod_fmod() {
assert_eq!(ev("+5"), "5");
assert_eq!(ev("- -5"), "5");
assert_eq!(ev("10 div 4"), "2.5");
assert_eq!(ev("10 mod 4"), "2");
assert_eq!(ev("10 fmod 4"), "2");
assert_eq!(ev("trunc 9.99"), "9");
}
#[test]
fn entity_unescaping() {
assert_eq!(ev("1 < 2"), "1");
assert_eq!(ev("2 > 1"), "1");
assert_eq!(ev("5 − 3"), "2");
assert_eq!(ev("5 \u{2212} 3"), "2");
}
#[test]
fn nan_result_stringifies() {
assert_eq!(ev("(1e300*1e300) - (1e300*1e300)"), "NAN");
assert_eq!(ev("-1e300*1e300"), "-INF");
}
#[test]
fn number_parsing_quirks() {
assert_eq!(ev(".5"), "0.5");
assert_eq!(ev("5."), "5");
assert_eq!(ev("1.2.3"), "1.2");
assert_eq!(ev("."), "0");
}
#[test]
fn multi_digit_exponent_formatting() {
assert_eq!(ev("1e100"), "1.0E+100");
assert_eq!(ev("1e-100"), "1.0E-100");
}
#[test]
fn trig_and_sqrt() {
assert_eq!(ev("cos(0) + sin(0)"), "1");
assert_eq!(ev("atan(0)"), "0");
assert_eq!(ev("acos(1)"), "0");
assert_eq!(ev("asin(1) = pi/2"), "1");
assert_eq!(ev("sqrt(9)"), "3");
}
#[test]
fn all_error_strings() {
assert_eq!(err("1 2"), "Expression error: Unexpected number.");
assert_eq!(err("1 pi"), "Expression error: Unexpected number.");
assert_eq!(err("foo"), "Expression error: Unrecognized word \"foo\".");
assert_eq!(err("FOO"), "Expression error: Unrecognized word \"foo\".");
assert_eq!(err("1 sin"), "Expression error: Unexpected sin operator.");
assert_eq!(err("1 ("), "Expression error: Unexpected ( operator.");
assert_eq!(err("<1"), "Expression error: Unexpected < operator.");
assert_eq!(err("!=1"), "Expression error: Unexpected != operator.");
assert_eq!(err("<>1"), "Expression error: Unexpected <> operator.");
assert_eq!(
err("1 + 1)"),
"Expression error: Unexpected closing bracket."
);
assert_eq!(err("(1"), "Expression error: Unclosed bracket.");
assert_eq!(
err("1, 2"),
"Expression error: Unrecognized punctuation character \",\"."
);
assert_eq!(
err("1 @ 2"),
"Expression error: Unrecognized punctuation character \"@\"."
);
assert_eq!(
err("1 \u{00d7} 2"),
"Expression error: Unrecognized punctuation character \"\u{00d7}\"."
);
assert_eq!(err("-"), "Expression error: Missing operand for -.");
assert_eq!(err("+"), "Expression error: Missing operand for +.");
assert_eq!(err("1*"), "Expression error: Missing operand for *.");
assert_eq!(err("1/"), "Expression error: Missing operand for /.");
assert_eq!(err("1 mod"), "Expression error: Missing operand for mod.");
assert_eq!(err("1 fmod"), "Expression error: Missing operand for fmod.");
assert_eq!(err("1+"), "Expression error: Missing operand for +.");
assert_eq!(err("1-"), "Expression error: Missing operand for -.");
assert_eq!(err("1 and"), "Expression error: Missing operand for and.");
assert_eq!(err("1 or"), "Expression error: Missing operand for or.");
assert_eq!(err("1 ="), "Expression error: Missing operand for =.");
assert_eq!(err("1 not"), "Expression error: Unexpected not operator.");
assert_eq!(
err("1 round"),
"Expression error: Missing operand for round."
);
assert_eq!(err("1<"), "Expression error: Missing operand for <.");
assert_eq!(err("1>"), "Expression error: Missing operand for >.");
assert_eq!(err("1<="), "Expression error: Missing operand for <=.");
assert_eq!(err("1>="), "Expression error: Missing operand for >=.");
assert_eq!(err("1<>"), "Expression error: Missing operand for <>.");
assert_eq!(err("sin()"), "Expression error: Missing operand for sin.");
assert_eq!(err("cos()"), "Expression error: Missing operand for cos.");
assert_eq!(err("tan()"), "Expression error: Missing operand for tan.");
assert_eq!(err("asin()"), "Expression error: Missing operand for asin.");
assert_eq!(err("acos()"), "Expression error: Missing operand for acos.");
assert_eq!(err("atan()"), "Expression error: Missing operand for atan.");
assert_eq!(err("exp()"), "Expression error: Missing operand for exp.");
assert_eq!(err("ln()"), "Expression error: Missing operand for ln.");
assert_eq!(err("abs()"), "Expression error: Missing operand for abs.");
assert_eq!(err("floor"), "Expression error: Missing operand for floor.");
assert_eq!(err("trunc"), "Expression error: Missing operand for trunc.");
assert_eq!(err("ceil"), "Expression error: Missing operand for ceil.");
assert_eq!(err("1 ^"), "Expression error: Missing operand for ^.");
assert_eq!(err("sqrt"), "Expression error: Missing operand for sqrt.");
assert_eq!(err("1 e"), "Expression error: Missing operand for e.");
assert_eq!(err("1/0"), "Division by zero.");
assert_eq!(err("1 mod 0"), "Division by zero.");
assert_eq!(err("1 fmod 0"), "Division by zero.");
assert_eq!(err("1 mod 0.5"), "Division by zero.");
assert_eq!(
err("asin(3)"),
"Invalid argument for asin: less than -1 or greater than 1."
);
assert_eq!(
err("acos(-1.1)"),
"Invalid argument for acos: less than -1 or greater than 1."
);
assert_eq!(
err("ln(-1)"),
"Invalid argument for ln: less than or equal to 0."
);
assert_eq!(
err("ln(0)"),
"Invalid argument for ln: less than or equal to 0."
);
assert_eq!(err("sqrt(-1)"), "In sqrt: Result is not a number.");
}
#[test]
fn stack_exhaustion_does_not_overflow() {
let deep_parens = format!("{}1{}", "(".repeat(1001), ")".repeat(1001));
assert_eq!(err(&deep_parens), "Expression error: Stack exhausted.");
let deep_calls = format!("{}1{}", "ln(".repeat(1001), ")".repeat(1001));
assert_eq!(err(&deep_calls), "Expression error: Stack exhausted.");
}
#[test]
fn do_operation_unreachable_ops_error() {
let mut stack = vec![1.0];
assert_eq!(
do_operation(Op::Pi, &mut stack).unwrap_err().message(),
"Expression error: Unknown error ()."
);
assert_eq!(
do_operation(Op::Open, &mut stack).unwrap_err().message(),
"Expression error: Unknown error ()."
);
assert_eq!(
do_operation(Op::Close, &mut stack).unwrap_err().message(),
"Expression error: Unknown error ()."
);
}
#[test]
fn helper_edge_cases() {
assert_eq!(format_double(f64::NAN), "NAN");
assert_eq!(format_double(f64::INFINITY), "INF");
assert_eq!(format_double(f64::NEG_INFINITY), "-INF");
assert_eq!(format_double(0.0), "0");
assert_eq!(intpow10(0), 1.0);
assert_eq!(intpow10(22), 1e22);
assert_eq!(intpow10(23), 10f64.powi(23));
assert_eq!(intpow10(-1), 0.1);
assert_eq!(intlog10abs(1000.0), 3);
assert_eq!(intlog10abs(0.0), 0);
assert_eq!(php_round(0.0, 2), 0.0);
assert!(php_round(f64::INFINITY, 2).is_infinite());
assert_eq!(php_round(5.0, 100), 5.0);
assert_eq!(round_helper(-2.5), -3.0);
assert_eq!(round_helper(2.5), 3.0);
}
#[test]
fn round_extreme_scale_branch() {
assert_eq!(php_round(1e-10, 23), 1e-10);
assert_eq!(php_round(5.0, 100), 5.0);
}
#[test]
fn whitespace_and_bare_number() {
assert_eq!(ev("\t 42 \n"), "42");
assert_eq!(ev("3"), "3");
}
#[test]
fn not_missing_operand_and_left_assoc_or() {
assert_eq!(err("not"), "Expression error: Missing operand for not.");
assert_eq!(ev("1 or 0 or 1"), "1");
}
#[test]
fn e_style_multi_digit_mantissa() {
assert_eq!(ev("1.5e20"), "1.5E+20");
assert_eq!(ev("1.25e-10"), "1.25E-10");
}
#[test]
fn precedence_and_name_tables_are_total() {
assert_eq!(precedence(Op::Pi), 0);
assert_eq!(precedence(Op::Close), -1);
assert_eq!(op_name(Op::Pi), "pi");
assert_eq!(op_name(Op::Open), "(");
assert_eq!(op_name(Op::Close), "(");
}
#[test]
fn php_round_branch_coverage() {
assert_eq!(php_round(1e14, 0), 1e14);
assert_eq!(php_round(1e15, -3), 1e15);
assert_eq!(php_round(1e10, -25), 1e10);
assert_eq!(intlog10abs(1e16), 16);
assert_eq!(intlog10abs(0.1), -1);
}
}