4. 简单计算器

任务说明

实现一个支持加(+)、减(-)、乘(*)、除(/)和括号的表达式计算器。输入为字符串形式的数学表达式,输出为计算结果。需要处理除数为零、非法字符、括号不匹配、运算符连续等错误输入,并给出明确错误提示。

实现方案

采用递归下降解析器实现。语法定义如下:

expr   := term (('+' | '-') term)*
term   := factor (('*' | '/') factor)*
factor := number | '(' expr ')' | '-' factor

解析器从左到右扫描输入,按优先级从低到高逐层解析。每个解析函数返回该层表达式的值,遇到语法错误或除零时立即返回错误信息。数字支持整数和浮点数(含小数点)。

代码实现

use std::fmt;

#[derive(Debug, Clone, PartialEq)]
pub enum CalcError {
    InvalidChar(char),
    UnexpectedEnd,
    MismatchedParen,
    DivisionByZero,
    InvalidExpression,
}

impl fmt::Display for CalcError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let msg = match self {
            CalcError::InvalidChar(c) => format!("非法字符: '{}'", c),
            CalcError::UnexpectedEnd => "表达式意外结束".to_string(),
            CalcError::MismatchedParen => "括号不匹配".to_string(),
            CalcError::DivisionByZero => "除数为零".to_string(),
            CalcError::InvalidExpression => "表达式格式错误".to_string(),
        };
        write!(f, "{}", msg)
    }
}

pub struct Parser {
    chars: Vec<char>,
    pos: usize,
}

impl Parser {
    pub fn new(input: &str) -> Self {
        Parser {
            chars: input.chars().collect(),
            pos: 0,
        }
    }

    fn peek(&self) -> Option<&char> {
        self.chars.get(self.pos)
    }

    fn next(&mut self) -> Option<&char> {
        let c = self.chars.get(self.pos);
        if c.is_some() {
            self.pos += 1;
        }
        c
    }

    fn skip_whitespace(&mut self) {
        while let Some(&c) = self.peek() {
            if c.is_whitespace() {
                self.pos += 1;
            } else {
                break;
            }
        }
    }

    fn parse_number(&mut self) -> Result<f64, CalcError> {
        self.skip_whitespace();
        let start = self.pos;
        let mut has_dot = false;

        while let Some(&c) = self.peek() {
            if c.is_ascii_digit() {
                self.pos += 1;
            } else if c == '.' && !has_dot {
                has_dot = true;
                self.pos += 1;
            } else {
                break;
            }
        }

        if start == self.pos {
            return Err(CalcError::InvalidExpression);
        }

        let num_str: String = self.chars[start..self.pos].iter().collect();
        num_str.parse::<f64>().map_err(|_| CalcError::InvalidExpression)
    }

    fn parse_factor(&mut self) -> Result<f64, CalcError> {
        self.skip_whitespace();

        match self.peek() {
            Some(&c) if c == '(' => {
                self.pos += 1; // 消费 '('
                let val = self.parse_expr()?;
                self.skip_whitespace();
                match self.next() {
                    Some(&')') => Ok(val),
                    _ => Err(CalcError::MismatchedParen),
                }
            }
            Some(&c) if c == '-' => {
                self.pos += 1;
                let val = self.parse_factor()?;
                Ok(-val)
            }
            Some(&c) if c.is_ascii_digit() || c == '.' => self.parse_number(),
            Some(&c) => Err(CalcError::InvalidChar(c)),
            None => Err(CalcError::UnexpectedEnd),
        }
    }

    fn parse_term(&mut self) -> Result<f64, CalcError> {
        let mut left = self.parse_factor()?;

        loop {
            self.skip_whitespace();
            match self.peek() {
                Some(&'*') => {
                    self.pos += 1;
                    let right = self.parse_factor()?;
                    left *= right;
                }
                Some(&'/') => {
                    self.pos += 1;
                    let right = self.parse_factor()?;
                    if right == 0.0 {
                        return Err(CalcError::DivisionByZero);
                    }
                    left /= right;
                }
                _ => break,
            }
        }
        Ok(left)
    }

    fn parse_expr(&mut self) -> Result<f64, CalcError> {
        let mut left = self.parse_term()?;

        loop {
            self.skip_whitespace();
            match self.peek() {
                Some(&'+') => {
                    self.pos += 1;
                    let right = self.parse_term()?;
                    left += right;
                }
                Some(&'-') => {
                    self.pos += 1;
                    let right = self.parse_term()?;
                    left -= right;
                }
                _ => break,
            }
        }
        Ok(left)
    }

    pub fn parse(&mut self) -> Result<f64, CalcError> {
        let val = self.parse_expr()?;
        self.skip_whitespace();
        if self.pos != self.chars.len() {
            if let Some(&c) = self.peek() {
                return Err(CalcError::InvalidChar(c));
            }
        }
        Ok(val)
    }
}

/// 计算表达式,成功返回 Ok(结果),失败返回 Err(错误信息)
pub fn calculate(expression: &str) -> Result<f64, CalcError> {
    let mut parser = Parser::new(expression);
    parser.parse()
}

fn main() {
    let test_cases = vec![
        "1 + 2 * 3",
        "(1 + 2) * 3",
        "10 / 4",
        "2 * (3 + 4) - 5",
        "-3 + 5",
        "1 / 0",
        "2 + * 3",
        "(1 + 2",
        "1 + abc",
        "3.5 * 2",
    ];

    for expr in test_cases {
        match calculate(expr) {
            Ok(val) => println!("{} = {}", expr, val),
            Err(e) => println!("{} => 错误: {}", expr, e),
        }
    }
}

落地与验证

将上述代码保存为 calculator.rs,使用 rustc calculator.rs && ./calculator 编译运行。程序会对内置测试用例逐一求值并打印结果。

预期输出(与代码逻辑严格一致):
1 + 2 * 3 = 7
(1 + 2) * 3 = 9
10 / 4 = 2.5
2 * (3 + 4) - 5 = 9
-3 + 5 = 2
1 / 0 => 错误: 除数为零
2 + * 3 => 错误: 表达式格式错误
(1 + 2 => 错误: 括号不匹配
1 + abc => 错误: 非法字符: 'a'
3.5 * 2 = 7

判定标准

  • 合法表达式(含括号、负数、小数、空格)计算结果正确,优先级与结合性符合数学规则;
  • 除数为零时返回 DivisionByZero 错误;
  • 非法字符、括号不匹配、运算符连续、表达式意外结束均返回对应错误信息,程序不崩溃;
  • 输入末尾存在多余字符时(如 1+2))能正确报错。
递归下降解析器调用结构 输入表达式 parse() parse_expr() parse_term() parse_factor() 递归调用 '(' 数字/负号 错误处理路径 非法字符 → InvalidChar | 除零 → DivisionByZero | 括号不匹配 → MismatchedParen | 意外结束 → UnexpectedEnd