0.0.1
This commit is contained in:
+425
@@ -0,0 +1,425 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user