pub const MAX_LEN: usize = 256;
const MAX_DEPTH: usize = 16;
const SIGNIFICANT_DIGITS: usize = 12;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CalcError {
Empty,
TooLong,
TooDeep,
UnbalancedParens,
Incomplete,
DivideByZero,
Unexpected(char),
OutOfRange,
}
impl CalcError {
pub fn message(&self) -> String {
match self {
Self::Empty => "nothing yet".to_string(),
Self::TooLong => "too long".to_string(),
Self::TooDeep => "too nested".to_string(),
Self::UnbalancedParens => "unclosed (".to_string(),
Self::Incomplete => "unfinished".to_string(),
Self::DivideByZero => "\u{00f7} by zero".to_string(),
Self::Unexpected(c) => format!("{c}?"),
Self::OutOfRange => "too large".to_string(),
}
}
}
pub fn evaluate(text: &str) -> Result<f64, CalcError> {
if text.len() > MAX_LEN {
return Err(CalcError::TooLong);
}
if text.trim().is_empty() {
return Err(CalcError::Empty);
}
let chars: Vec<char> = text.chars().collect();
let mut parser = Parser {
chars: &chars,
at: 0,
depth: 0,
};
let value = parser.expr()?;
parser.skip_spaces();
if parser.at < parser.chars.len() {
return Err(match parser.chars[parser.at] {
')' => CalcError::UnbalancedParens,
c => CalcError::Unexpected(c),
});
}
if !value.is_finite() {
return Err(CalcError::OutOfRange);
}
Ok(value)
}
struct Parser<'a> {
chars: &'a [char],
at: usize,
depth: usize,
}
impl Parser<'_> {
fn skip_spaces(&mut self) {
while self.chars.get(self.at) == Some(&' ') {
self.at += 1;
}
}
fn peek(&mut self) -> Option<char> {
self.skip_spaces();
self.chars.get(self.at).copied()
}
fn expr(&mut self) -> Result<f64, CalcError> {
let mut value = self.term()?;
while let Some(op @ ('+' | '-')) = self.peek() {
self.at += 1;
let rhs = self.term()?;
value = if op == '+' { value + rhs } else { value - rhs };
}
Ok(value)
}
fn term(&mut self) -> Result<f64, CalcError> {
let mut value = self.factor()?;
while let Some(op @ ('*' | '/' | 'x' | '\u{00D7}' | '\u{00F7}')) = self.peek() {
self.at += 1;
let rhs = self.factor()?;
if matches!(op, '*' | 'x' | '\u{00D7}') {
value *= rhs;
} else {
if rhs == 0.0 {
return Err(CalcError::DivideByZero);
}
value /= rhs;
}
}
Ok(value)
}
fn factor(&mut self) -> Result<f64, CalcError> {
match self.peek() {
None | Some(')') => Err(CalcError::Incomplete),
Some('-') => {
self.at += 1;
Ok(-self.factor()?)
}
Some('+') => {
self.at += 1;
self.factor()
}
Some('(') => {
self.at += 1;
self.depth += 1;
if self.depth > MAX_DEPTH {
return Err(CalcError::TooDeep);
}
let value = self.expr()?;
if self.peek() != Some(')') {
return Err(CalcError::UnbalancedParens);
}
self.at += 1;
self.depth -= 1;
Ok(value)
}
Some(c) if c.is_ascii_digit() || c == '.' => self.number(),
Some(c) => Err(CalcError::Unexpected(c)),
}
}
fn number(&mut self) -> Result<f64, CalcError> {
let start = self.at;
let mut seen_dot = false;
while let Some(&c) = self.chars.get(self.at) {
if c.is_ascii_digit() {
self.at += 1;
} else if c == '.' && !seen_dot {
seen_dot = true;
self.at += 1;
} else {
break;
}
}
let text: String = self.chars[start..self.at].iter().collect();
text.parse::<f64>().map_err(|_| CalcError::Incomplete)
}
}
pub fn format_result(value: f64) -> String {
if value == 0.0 {
return "0".to_string();
}
if !value.is_finite() {
return "—".to_string();
}
let text = format!("{value:.prec$e}", prec = SIGNIFICANT_DIGITS - 1);
let rounded: f64 = text.parse().unwrap_or(value);
let mut out = format!("{rounded}");
if out.contains('e') {
return out;
}
if out.contains('.') {
while out.ends_with('0') {
out.pop();
}
if out.ends_with('.') {
out.pop();
}
}
out
}
pub fn fit_result(value: f64, width: usize) -> String {
let plain = format_result(value);
if plain.chars().count() <= width {
return plain;
}
for digits in (0..=SIGNIFICANT_DIGITS.saturating_sub(1)).rev() {
let candidate = format!("{value:.digits$e}");
if candidate.chars().count() <= width {
return candidate;
}
}
"—".to_string()
}
#[allow(clippy::float_cmp)]
#[cfg(test)]
mod tests {
use super::*;
fn ok(text: &str) -> f64 {
evaluate(text).unwrap_or_else(|e| panic!("{text:?} failed: {:?}", e.message()))
}
#[test]
fn precedence_is_the_arithmetic_kind() {
assert_eq!(ok("2 + 3 * 4"), 14.0);
assert_eq!(ok("2 * 3 + 4"), 10.0);
assert_eq!(ok("100 - 10 / 2"), 95.0);
}
#[test]
fn brackets_override_precedence() {
assert_eq!(ok("(2 + 3) * 4"), 20.0);
assert_eq!(ok("((1 + 2) * (3 + 4))"), 21.0);
}
#[test]
fn the_display_hides_the_binary_representation() {
assert_eq!(format_result(ok("0.1 + 0.2")), "0.3");
assert_eq!(format_result(ok("1 / 3")), "0.333333333333");
assert_eq!(format_result(ok("2 / 2")), "1");
assert_eq!(format_result(ok("10 * 10")), "100");
}
#[test]
fn a_negative_zero_is_shown_as_zero() {
assert_eq!(format_result(ok("0 * -1")), "0");
}
#[test]
fn unary_minus_binds_tighter_than_the_operators() {
assert_eq!(ok("-5 + 3"), -2.0);
assert_eq!(ok("3 - -5"), 8.0);
assert_eq!(ok("-(2 + 3)"), -5.0);
}
#[test]
fn the_alternative_operator_glyphs_work() {
assert_eq!(ok("6 x 7"), 42.0);
assert_eq!(ok("6 \u{00D7} 7"), 42.0);
assert_eq!(ok("84 \u{00F7} 2"), 42.0);
}
#[test]
fn dividing_by_zero_is_an_error_rather_than_infinity() {
assert_eq!(evaluate("1 / 0"), Err(CalcError::DivideByZero));
assert_eq!(evaluate("1 / (2 - 2)"), Err(CalcError::DivideByZero));
}
#[test]
fn a_half_typed_expression_reads_as_unfinished() {
assert_eq!(evaluate("2 +"), Err(CalcError::Incomplete));
assert_eq!(evaluate("2 * "), Err(CalcError::Incomplete));
assert_eq!(evaluate("(2 + 3"), Err(CalcError::UnbalancedParens));
}
#[test]
fn a_stray_closing_bracket_is_refused() {
assert_eq!(evaluate("2 + 3)"), Err(CalcError::UnbalancedParens));
assert_eq!(evaluate(")"), Err(CalcError::Incomplete));
}
#[test]
fn nonsense_names_the_character() {
assert_eq!(evaluate("2 & 3"), Err(CalcError::Unexpected('&')));
assert!(
CalcError::Unexpected('&').message().contains('&'),
"the message has to name what was typed"
);
}
#[test]
fn deep_nesting_is_refused_rather_than_overflowing_the_stack() {
let deep = format!("{}1{}", "(".repeat(120), ")".repeat(120));
assert_eq!(evaluate(&deep), Err(CalcError::TooDeep));
let ordinary = format!("{}1{}", "(".repeat(8), ")".repeat(8));
assert_eq!(ok(&ordinary), 1.0);
}
#[test]
fn an_over_long_expression_is_refused_by_length() {
let long = "1+".repeat(MAX_LEN);
assert_eq!(evaluate(&long), Err(CalcError::TooLong));
}
#[test]
fn a_non_finite_result_is_never_printed_as_a_number() {
assert_eq!(format_result(f64::INFINITY), "\u{2014}");
assert_eq!(format_result(f64::NEG_INFINITY), "\u{2014}");
assert_eq!(format_result(f64::NAN), "\u{2014}");
let widest = "99 * ".repeat(50) + "99";
assert!(widest.len() <= MAX_LEN);
assert!(
evaluate(&widest).is_ok_and(f64::is_finite),
"the longest product that fits must still be a finite number"
);
}
#[test]
fn a_result_too_wide_goes_scientific_rather_than_being_cut() {
let value = 123_456_789_012.0;
let plain = format_result(value);
assert!(
plain.chars().count() > 8,
"the fixture must not already fit"
);
let fitted = fit_result(value, 8);
assert!(
fitted.chars().count() <= 8,
"{fitted:?} does not fit in 8 cells"
);
assert!(
fitted.contains('e'),
"a number that will not fit must become scientific, not truncated; got {fitted:?}"
);
assert!(
!fitted.starts_with("1234"),
"{fitted:?} is a prefix of the real answer, which reads as a different number"
);
}
#[test]
fn a_result_that_fits_is_left_alone() {
assert_eq!(fit_result(42.0, 20), "42");
assert_eq!(fit_result(0.3, 20), "0.3");
}
#[test]
fn an_impossible_width_gives_up_rather_than_lying() {
let out = fit_result(123_456_789_012.0, 2);
assert!(
!out.chars().next().is_some_and(|c| c.is_ascii_digit()),
"{out:?} starts with a digit, which reads as an answer"
);
}
#[test]
fn generated_expressions_never_panic_or_hang() {
const PIECES: &[&str] = &[
"1",
"0",
".",
"..",
"+",
"-",
"*",
"/",
"(",
")",
" ",
"x",
"\u{00D7}",
"\u{00F7}",
"99999999",
"0.0000001",
"&",
"\u{65E5}",
"\u{1F31E}",
"\u{0301}",
"e",
"--",
"()",
];
let mut seed = 12_345u64;
for _ in 0..20_000 {
let mut text = String::new();
seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
let count = (seed >> 33) as usize % 12;
for i in 0..count {
seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
text.push_str(PIECES[(seed >> 33) as usize % PIECES.len()]);
let _ = i;
}
if let Ok(value) = evaluate(&text) {
assert!(
value.is_finite(),
"{text:?} evaluated to {value}, which should have been an error"
);
let _ = format_result(value);
for width in [0usize, 1, 3, 8, 40] {
let fitted = fit_result(value, width);
assert!(
fitted.chars().count() <= width.max(1),
"{text:?} at width {width} produced {fitted:?}"
);
}
}
}
}
}