Files
RCalc/src/engine.rs
T
2026-07-31 20:16:28 +03:00

426 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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"));
}
}