pub fn eval_calc(text: &str) -> Option<f64> {
let clean = normalize(text);
let toks = tokenize(&clean)?;
let num_count = toks.iter().filter(|t| matches!(t, Tok::Num(_))).count();
if num_count < 2 {
return None; }
let mut p = Parser { toks, pos: 0 };
let v = p.expr()?;
if p.pos == p.toks.len() { Some(v) } else { None }
}
pub fn calc_answer_string(v: f64) -> String {
if v.fract().abs() < 1e-9 {
format!("{}", v.round() as i64)
} else {
let s = format!("{v:.4}");
s.trim_end_matches('0').trim_end_matches('.').to_string()
}
}
#[derive(Debug, Clone, PartialEq)]
enum Tok {
Num(f64),
Op(char),
}
fn normalize(text: &str) -> String {
let mut work: Vec<char> = Vec::with_capacity(text.len());
for ch in text.chars() {
let mapped = match ch {
'0'..='9' | '+' | '-' | '*' | '/' | '.' | ' ' => ch,
'0'..='9' => char::from_digit(ch as u32 - '0' as u32, 10).unwrap_or(' '),
'+' | '加' => '+',
'-' | '—' | '﹣' | 'ー' | '减' | '減' => '-',
'×' | '*' | '✕' | '⨯' | '乘' => '*',
'÷' | '/' | '➗' | '除' => '/',
_ if is_cjk_numeral(ch) => ch,
_ => ' ',
};
work.push(mapped);
}
let mut out = String::with_capacity(work.len());
let mut i = 0;
while i < work.len() {
if is_cjk_numeral(work[i]) {
let start = i;
while i < work.len() && is_cjk_numeral(work[i]) {
i += 1;
}
if let Some(n) = cjk_run_to_number(&work[start..i]) {
out.push_str(&n.to_string());
} else {
out.push(' ');
}
} else {
out.push(work[i]);
i += 1;
}
}
out
}
fn is_cjk_numeral(c: char) -> bool {
matches!(
c,
'零' | '〇'
| '一'
| '二'
| '两'
| '三'
| '四'
| '五'
| '六'
| '七'
| '八'
| '九'
| '十'
| '壹'
| '贰'
| '叁'
| '肆'
| '伍'
| '陆'
| '柒'
| '捌'
| '玖'
| '拾'
)
}
fn cjk_digit(c: char) -> Option<i64> {
Some(match c {
'零' | '〇' => 0,
'一' | '壹' => 1,
'二' | '两' | '贰' => 2,
'三' | '叁' => 3,
'四' | '肆' => 4,
'五' | '伍' => 5,
'六' | '陆' => 6,
'七' | '柒' => 7,
'八' | '捌' => 8,
'九' | '玖' => 9,
_ => return None,
})
}
fn cjk_run_to_number(run: &[char]) -> Option<i64> {
if let Some(pos) = run.iter().position(|&c| c == '十' || c == '拾') {
let left = &run[..pos];
let right = &run[pos + 1..];
let tens = if left.is_empty() {
1
} else {
cjk_digit(*left.last()?)?
};
let units = if right.is_empty() {
0
} else {
cjk_digit(*right.first()?)?
};
Some(tens * 10 + units)
} else {
let mut acc = 0i64;
let mut any = false;
for &c in run {
acc = acc * 10 + cjk_digit(c)?;
any = true;
}
if any { Some(acc) } else { None }
}
}
fn tokenize(s: &str) -> Option<Vec<Tok>> {
let chars: Vec<char> = s.chars().collect();
let mut toks = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == ' ' {
i += 1;
} else if c.is_ascii_digit() || c == '.' {
let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
i += 1;
}
let num: f64 = chars[start..i].iter().collect::<String>().parse().ok()?;
toks.push(Tok::Num(num));
} else if matches!(c, '+' | '-' | '*' | '/') {
toks.push(Tok::Op(c));
i += 1;
} else {
return None;
}
}
Some(toks)
}
struct Parser {
toks: Vec<Tok>,
pos: usize,
}
impl Parser {
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.pos)
}
fn expr(&mut self) -> Option<f64> {
let mut v = self.term()?;
while let Some(Tok::Op(c @ ('+' | '-'))) = self.peek() {
let c = *c;
self.pos += 1;
let r = self.term()?;
v = if c == '+' { v + r } else { v - r };
}
Some(v)
}
fn term(&mut self) -> Option<f64> {
let mut v = self.factor()?;
while let Some(Tok::Op(c @ ('*' | '/'))) = self.peek() {
let c = *c;
self.pos += 1;
let r = self.factor()?;
if c == '/' {
if r == 0.0 {
return None;
}
v /= r;
} else {
v *= r;
}
}
Some(v)
}
fn factor(&mut self) -> Option<f64> {
match self.peek()? {
Tok::Op('-') => {
self.pos += 1;
Some(-self.factor()?)
}
Tok::Op('+') => {
self.pos += 1;
self.factor()
}
Tok::Num(n) => {
let n = *n;
self.pos += 1;
Some(n)
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_expressions() {
assert_eq!(eval_calc("3+5"), Some(8.0));
assert_eq!(eval_calc("3+5=?"), Some(8.0));
assert_eq!(eval_calc("12-4="), Some(8.0));
assert_eq!(eval_calc("3*2"), Some(6.0));
assert_eq!(eval_calc("6/2=?"), Some(3.0));
assert_eq!(eval_calc("9-3-2"), Some(4.0)); assert_eq!(eval_calc("2+3*4"), Some(14.0)); assert_eq!(eval_calc("-3+5"), Some(2.0)); }
#[test]
fn fullwidth_and_symbol_operators() {
assert_eq!(eval_calc("3+5=?"), Some(8.0));
assert_eq!(eval_calc("7×8"), Some(56.0));
assert_eq!(eval_calc("8÷4=?"), Some(2.0));
}
#[test]
fn chinese_numerals_and_operators() {
assert_eq!(eval_calc("三加五等于"), Some(8.0));
assert_eq!(eval_calc("十二减四"), Some(8.0));
assert_eq!(eval_calc("二十三加七"), Some(30.0));
assert_eq!(eval_calc("十五除以三"), Some(5.0));
assert_eq!(eval_calc("两乘以九"), Some(18.0));
}
#[test]
fn noise_is_ignored() {
assert_eq!(eval_calc("请计算 3 + 5 = ? 的结果"), Some(8.0));
assert_eq!(eval_calc("答案:12*2"), Some(24.0));
}
#[test]
fn rejects_non_calc() {
assert_eq!(eval_calc(""), None);
assert_eq!(eval_calc("abc"), None);
assert_eq!(eval_calc("5"), None); assert_eq!(eval_calc("1/0"), None); assert_eq!(eval_calc("3 5"), None); }
#[test]
fn answer_formatting() {
assert_eq!(calc_answer_string(8.0), "8");
assert_eq!(calc_answer_string(-3.0), "-3");
assert_eq!(calc_answer_string(2.5), "2.5");
}
}