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 = 910 / 4 = 2.52 * (3 + 4) - 5 = 9-3 + 5 = 21 / 0 => 错误: 除数为零2 + * 3 => 错误: 表达式格式错误(1 + 2 => 错误: 括号不匹配1 + abc => 错误: 非法字符: 'a'3.5 * 2 = 7
判定标准
- 合法表达式(含括号、负数、小数、空格)计算结果正确,优先级与结合性符合数学规则;
- 除数为零时返回
DivisionByZero错误; - 非法字符、括号不匹配、运算符连续、表达式意外结束均返回对应错误信息,程序不崩溃;
- 输入末尾存在多余字符时(如
1+2))能正确报错。