426 lines
9.8 KiB
Rust
426 lines
9.8 KiB
Rust
//! Calculator logic (standard / Windows-like).
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum Op {
|
||
Add,
|
||
Sub,
|
||
Mul,
|
||
Div,
|
||
}
|
||
|
||
impl Op {
|
||
pub fn symbol(self) -> &'static str {
|
||
match self {
|
||
Op::Add => "+",
|
||
Op::Sub => "−",
|
||
Op::Mul => "×",
|
||
Op::Div => "÷",
|
||
}
|
||
}
|
||
|
||
fn apply(self, a: f64, b: f64) -> Result<f64, CalcError> {
|
||
let r = match self {
|
||
Op::Add => a + b,
|
||
Op::Sub => a - b,
|
||
Op::Mul => a * b,
|
||
Op::Div => {
|
||
if b == 0.0 {
|
||
return Err(CalcError::DivByZero);
|
||
}
|
||
a / b
|
||
}
|
||
};
|
||
if !r.is_finite() {
|
||
Err(CalcError::Overflow)
|
||
} else {
|
||
Ok(r)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum CalcError {
|
||
DivByZero,
|
||
Overflow,
|
||
Invalid,
|
||
}
|
||
|
||
impl CalcError {
|
||
pub fn message(self) -> &'static str {
|
||
match self {
|
||
CalcError::DivByZero => "Cannot divide by zero",
|
||
CalcError::Overflow => "Overflow",
|
||
CalcError::Invalid => "Invalid input",
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct Calculator {
|
||
/// Digits currently being entered (or last result as string).
|
||
entry: String,
|
||
/// True while the user is typing a new number.
|
||
typing: bool,
|
||
/// Left operand waiting for the next number / equals.
|
||
pending: Option<f64>,
|
||
/// Pending binary operator.
|
||
op: Option<Op>,
|
||
/// Expression line above the main display (e.g. "12 +").
|
||
expression: String,
|
||
/// Last error, if any.
|
||
error: Option<CalcError>,
|
||
/// Memory register.
|
||
memory: f64,
|
||
/// Whether memory has been set (for UI hint).
|
||
memory_set: bool,
|
||
}
|
||
|
||
impl Default for Calculator {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
impl Calculator {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
entry: "0".into(),
|
||
typing: false,
|
||
pending: None,
|
||
op: None,
|
||
expression: String::new(),
|
||
error: None,
|
||
memory: 0.0,
|
||
memory_set: false,
|
||
}
|
||
}
|
||
|
||
pub fn display(&self) -> &str {
|
||
if let Some(err) = self.error {
|
||
return err.message();
|
||
}
|
||
&self.entry
|
||
}
|
||
|
||
pub fn expression(&self) -> &str {
|
||
&self.expression
|
||
}
|
||
|
||
pub fn has_memory(&self) -> bool {
|
||
self.memory_set
|
||
}
|
||
|
||
pub fn clear_all(&mut self) {
|
||
*self = Self {
|
||
memory: self.memory,
|
||
memory_set: self.memory_set,
|
||
..Self::new()
|
||
};
|
||
}
|
||
|
||
pub fn clear_entry(&mut self) {
|
||
self.error = None;
|
||
self.entry = "0".into();
|
||
self.typing = false;
|
||
}
|
||
|
||
pub fn backspace(&mut self) {
|
||
if self.error.is_some() {
|
||
self.clear_entry();
|
||
return;
|
||
}
|
||
if !self.typing {
|
||
return;
|
||
}
|
||
self.entry.pop();
|
||
if self.entry.is_empty() || self.entry == "-" {
|
||
self.entry = "0".into();
|
||
self.typing = false;
|
||
}
|
||
}
|
||
|
||
pub fn input_digit(&mut self, d: char) {
|
||
debug_assert!(d.is_ascii_digit());
|
||
if self.error.is_some() {
|
||
self.clear_all();
|
||
}
|
||
if !self.typing {
|
||
self.entry = d.to_string();
|
||
self.typing = true;
|
||
return;
|
||
}
|
||
if self.entry == "0" {
|
||
self.entry = d.to_string();
|
||
} else if self.entry == "-0" {
|
||
self.entry = format!("-{d}");
|
||
} else if digit_count(&self.entry) < 16 {
|
||
self.entry.push(d);
|
||
}
|
||
}
|
||
|
||
pub fn input_dot(&mut self) {
|
||
if self.error.is_some() {
|
||
self.clear_all();
|
||
}
|
||
if !self.typing {
|
||
self.entry = "0.".into();
|
||
self.typing = true;
|
||
return;
|
||
}
|
||
if !self.entry.contains('.') {
|
||
self.entry.push('.');
|
||
}
|
||
}
|
||
|
||
pub fn negate(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
if let Ok(v) = self.current_value() {
|
||
self.set_entry(-v);
|
||
self.typing = true;
|
||
}
|
||
}
|
||
|
||
pub fn percent(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
let Ok(cur) = self.current_value() else {
|
||
return;
|
||
};
|
||
let result = match (self.pending, self.op) {
|
||
(Some(a), Some(Op::Add | Op::Sub)) => a * cur / 100.0,
|
||
_ => cur / 100.0,
|
||
};
|
||
self.set_entry(result);
|
||
self.typing = false;
|
||
}
|
||
|
||
pub fn reciprocal(&mut self) {
|
||
self.unary(|x| {
|
||
if x == 0.0 {
|
||
Err(CalcError::DivByZero)
|
||
} else {
|
||
Ok(1.0 / x)
|
||
}
|
||
});
|
||
}
|
||
|
||
pub fn square(&mut self) {
|
||
self.unary(|x| {
|
||
let r = x * x;
|
||
if r.is_finite() {
|
||
Ok(r)
|
||
} else {
|
||
Err(CalcError::Overflow)
|
||
}
|
||
});
|
||
}
|
||
|
||
pub fn sqrt(&mut self) {
|
||
self.unary(|x| {
|
||
if x < 0.0 {
|
||
Err(CalcError::Invalid)
|
||
} else {
|
||
Ok(x.sqrt())
|
||
}
|
||
});
|
||
}
|
||
|
||
pub fn set_op(&mut self, op: Op) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
if let Err(e) = self.commit_pending() {
|
||
self.fail(e);
|
||
return;
|
||
}
|
||
let Ok(v) = self.current_value() else {
|
||
return;
|
||
};
|
||
self.pending = Some(v);
|
||
self.op = Some(op);
|
||
self.expression = format!("{} {}", format_number(v), op.symbol());
|
||
self.typing = false;
|
||
}
|
||
|
||
pub fn equals(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
let Some(op) = self.op else {
|
||
self.expression.clear();
|
||
self.typing = false;
|
||
return;
|
||
};
|
||
let Ok(b) = self.current_value() else {
|
||
return;
|
||
};
|
||
let a = self.pending.unwrap_or(b);
|
||
match op.apply(a, b) {
|
||
Ok(r) => {
|
||
self.expression = format!(
|
||
"{} {} {} =",
|
||
format_number(a),
|
||
op.symbol(),
|
||
format_number(b)
|
||
);
|
||
self.set_entry(r);
|
||
self.pending = None;
|
||
self.op = None;
|
||
self.typing = false;
|
||
}
|
||
Err(e) => self.fail(e),
|
||
}
|
||
}
|
||
|
||
// --- Memory ---
|
||
|
||
pub fn memory_clear(&mut self) {
|
||
self.memory = 0.0;
|
||
self.memory_set = false;
|
||
}
|
||
|
||
pub fn memory_recall(&mut self) {
|
||
if self.error.is_some() {
|
||
self.clear_all();
|
||
}
|
||
self.set_entry(self.memory);
|
||
self.typing = false;
|
||
}
|
||
|
||
pub fn memory_add(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
if let Ok(v) = self.current_value() {
|
||
self.memory += v;
|
||
self.memory_set = true;
|
||
self.typing = false;
|
||
}
|
||
}
|
||
|
||
pub fn memory_sub(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
if let Ok(v) = self.current_value() {
|
||
self.memory -= v;
|
||
self.memory_set = true;
|
||
self.typing = false;
|
||
}
|
||
}
|
||
|
||
pub fn memory_store(&mut self) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
if let Ok(v) = self.current_value() {
|
||
self.memory = v;
|
||
self.memory_set = true;
|
||
self.typing = false;
|
||
}
|
||
}
|
||
|
||
// --- internals ---
|
||
|
||
fn unary(&mut self, f: impl FnOnce(f64) -> Result<f64, CalcError>) {
|
||
if self.error.is_some() {
|
||
return;
|
||
}
|
||
let Ok(v) = self.current_value() else {
|
||
return;
|
||
};
|
||
match f(v) {
|
||
Ok(r) => {
|
||
self.expression.clear();
|
||
self.set_entry(r);
|
||
self.typing = false;
|
||
}
|
||
Err(e) => self.fail(e),
|
||
}
|
||
}
|
||
|
||
fn commit_pending(&mut self) -> Result<(), CalcError> {
|
||
let (Some(a), Some(op)) = (self.pending, self.op) else {
|
||
return Ok(());
|
||
};
|
||
if !self.typing {
|
||
return Ok(());
|
||
}
|
||
let b = self.current_value()?;
|
||
let r = op.apply(a, b)?;
|
||
self.set_entry(r);
|
||
self.pending = Some(r);
|
||
self.op = None;
|
||
Ok(())
|
||
}
|
||
|
||
fn current_value(&self) -> Result<f64, CalcError> {
|
||
self.entry
|
||
.parse::<f64>()
|
||
.map_err(|_| CalcError::Invalid)
|
||
}
|
||
|
||
fn set_entry(&mut self, v: f64) {
|
||
self.entry = format_number(v);
|
||
self.error = None;
|
||
}
|
||
|
||
fn fail(&mut self, e: CalcError) {
|
||
self.error = Some(e);
|
||
self.pending = None;
|
||
self.op = None;
|
||
self.expression.clear();
|
||
self.typing = false;
|
||
}
|
||
}
|
||
|
||
fn digit_count(s: &str) -> usize {
|
||
s.chars().filter(|c| c.is_ascii_digit()).count()
|
||
}
|
||
|
||
fn format_number(v: f64) -> String {
|
||
if !v.is_finite() {
|
||
return "0".into();
|
||
}
|
||
// Prefer integer display when close enough.
|
||
if v.fract().abs() < 1e-12 && v.abs() < 1e15 {
|
||
return format!("{}", v as i64);
|
||
}
|
||
let s = format!("{:.12}", v);
|
||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
||
if s.is_empty() || s == "-" {
|
||
"0".into()
|
||
} else {
|
||
s
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn basic_add() {
|
||
let mut c = Calculator::new();
|
||
c.input_digit('1');
|
||
c.input_digit('2');
|
||
c.set_op(Op::Add);
|
||
c.input_digit('3');
|
||
c.equals();
|
||
assert_eq!(c.display(), "15");
|
||
}
|
||
|
||
#[test]
|
||
fn div_by_zero() {
|
||
let mut c = Calculator::new();
|
||
c.input_digit('1');
|
||
c.set_op(Op::Div);
|
||
c.input_digit('0');
|
||
c.equals();
|
||
assert!(c.display().contains("zero"));
|
||
}
|
||
}
|