prejsx_math 0.1.0

A minimal math expression parser using only Rust's standard library
Documentation
//! # prejsx_math
//!
//! A minimal math expression parser using only Rust's standard library.
//! Supports +, -, *, /, parentheses, and negative numbers.

type CharIter<'a> = std::iter::Peekable<std::str::Chars<'a>>;

/// Evaluates a simple arithmetic expression using +, -, *, /, and parentheses.
///
/// # Examples
/// ```
/// use prejsx_math::eval_math;
/// assert_eq!(eval_math("1 + 2").unwrap(), 3.0);
/// assert_eq!(eval_math("2 * (3 + 1)").unwrap(), 8.0);
/// ```
pub fn eval_math(input: &str) -> Result<f64, String> {
    let mut chars = input.trim().chars().peekable();
    let result = parse_expr(&mut chars)?;
    skip_whitespace(&mut chars);
    if chars.peek().is_some() {
        let remaining: String = chars.collect();
        return Err(format!("Unexpected characters at end: '{}'", remaining));
    }
    Ok(result)
}

fn skip_whitespace(chars: &mut CharIter) {
    while let Some(&c) = chars.peek() {
        if c.is_whitespace() {
            chars.next();
        } else {
            break;
        }
    }
}

fn parse_expr(chars: &mut CharIter) -> Result<f64, String> {
    let mut value = parse_term(chars)?;
    loop {
        skip_whitespace(chars);
        match chars.peek() {
            Some(&'+') => {
                chars.next();
                value += parse_term(chars)?;
            }
            Some(&'-') => {
                chars.next();
                value -= parse_term(chars)?;
            }
            _ => break,
        }
    }
    Ok(value)
}

fn parse_term(chars: &mut CharIter) -> Result<f64, String> {
    let mut value = parse_factor(chars)?;
    loop {
        skip_whitespace(chars);
        match chars.peek() {
            Some(&'*') => {
                chars.next();
                value *= parse_factor(chars)?;
            }
            Some(&'/') => {
                chars.next();
                value /= parse_factor(chars)?;
            }
            _ => break,
        }
    }
    Ok(value)
}

fn parse_factor(chars: &mut CharIter) -> Result<f64, String> {
    skip_whitespace(chars);

    if let Some(&'(') = chars.peek() {
        chars.next(); // consume '('
        let value = parse_expr(chars)?;
        skip_whitespace(chars);
        if chars.next() != Some(')') {
            return Err("Expected ')'".into());
        }
        return Ok(value);
    }

    let mut negative = false;
    if let Some(&'-') = chars.peek() {
        chars.next();
        negative = true;
    }

    let mut num_str = String::new();
    while let Some(&c) = chars.peek() {
        if c.is_ascii_digit() || c == '.' {
            num_str.push(c);
            chars.next();
        } else {
            break;
        }
    }

    if num_str.is_empty() {
        return Err("Expected number".into());
    }

    let value = num_str.parse::<f64>().map_err(|_| format!("Invalid number: {}", num_str))?;
    Ok(if negative { -value } else { value })
}