use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
UndefinedVariable,
DivisionByZero,
MismatchedParens,
Syntax,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvalError {
pub code: ErrorCode,
pub detail: String,
}
impl EvalError {
pub fn undefined_variable(name: &str) -> Self {
Self {
code: ErrorCode::UndefinedVariable,
detail: name.to_string(),
}
}
pub fn division_by_zero() -> Self {
Self {
code: ErrorCode::DivisionByZero,
detail: String::new(),
}
}
pub fn mismatched_parens() -> Self {
Self {
code: ErrorCode::MismatchedParens,
detail: String::new(),
}
}
pub fn syntax(message: impl Into<String>) -> Self {
Self {
code: ErrorCode::Syntax,
detail: message.into(),
}
}
pub fn other(message: impl Into<String>) -> Self {
Self {
code: ErrorCode::Other,
detail: message.into(),
}
}
pub fn from_message(message: &str) -> Self {
if message.contains("undefined variable:") {
let name = message.split_once(':').map_or("", |(_, name)| name).trim();
Self::undefined_variable(name)
} else if message.contains("division by zero") {
Self::division_by_zero()
} else if message.contains("mismatched parentheses") {
Self::mismatched_parens()
} else if message.contains("invalid number") || message.contains("unexpected") {
Self::syntax(message)
} else {
Self::other(message)
}
}
pub fn short(&self) -> String {
match self.code {
ErrorCode::UndefinedVariable => format!("undef: {}", self.detail),
ErrorCode::DivisionByZero => "div/0".to_string(),
ErrorCode::MismatchedParens => "parens".to_string(),
ErrorCode::Syntax => "syntax".to_string(),
ErrorCode::Other => "error".to_string(),
}
}
}
impl fmt::Display for EvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.code {
ErrorCode::UndefinedVariable => write!(f, "undefined variable: {}", self.detail),
ErrorCode::DivisionByZero => f.write_str("division by zero"),
ErrorCode::MismatchedParens => f.write_str("mismatched parentheses"),
ErrorCode::Syntax | ErrorCode::Other => f.write_str(&self.detail),
}
}
}
impl std::error::Error for EvalError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn long_messages() {
assert_eq!(
EvalError::undefined_variable("x").to_string(),
"undefined variable: x"
);
assert_eq!(
EvalError::division_by_zero().to_string(),
"division by zero"
);
assert_eq!(
EvalError::mismatched_parens().to_string(),
"mismatched parentheses"
);
assert_eq!(
EvalError::syntax("invalid number: 1x").to_string(),
"invalid number: 1x"
);
}
#[test]
fn short_codes_from_messages() {
let cases = [
("undefined variable: x", "undef: x"),
("undefined variable: tax_rate", "undef: tax_rate"),
("division by zero", "div/0"),
("mismatched parentheses", "parens"),
("invalid number: 1.2.3", "syntax"),
(
"invalid number format: 12,34 (commas must be proper thousand separators)",
"syntax",
),
("unexpected character: $", "syntax"),
("unexpected token", "syntax"),
("empty expression", "error"),
("invalid expression", "error"),
("expected number after unary minus", "error"),
(
"'sum' is a reserved keyword and cannot be used as a variable name",
"error",
),
("", "error"),
];
for (message, want) in cases {
let err = EvalError::from_message(message);
assert_eq!(err.short(), want, "{message:?}");
assert_eq!(err.to_string(), message, "round trip {message:?}");
}
}
}