//! Calculator logic (standard / formula). //! //! Uses exact rational arithmetic when possible so that e.g. `1÷3×3 = 1` //! instead of a float residue like `0.999999999999`. use std::fmt; #[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 => "÷", } } } #[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, Copy, PartialEq, Eq)] pub enum StdPanel { /// Classic immediate calculator (Windows-like chaining). Standard, /// Build a full expression, evaluate on `=` with operator precedence. Formula, } /// Exact rational number (always reduced, denominator > 0). #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct Rational { num: i128, den: i128, } impl Rational { fn new(num: i128, den: i128) -> Result { if den == 0 { return Err(CalcError::DivByZero); } let (mut num, mut den) = (num, den); if den < 0 { num = -num; den = -den; } let g = gcd(num.unsigned_abs(), den.unsigned_abs()); Ok(Self { num: num / g as i128, den: den / g as i128, }) } fn from_i128(n: i128) -> Self { Self { num: n, den: 1 } } /// Parse a decimal literal like `12`, `-3.5`, `0.001`, `1.5e-9` into an /// exact fraction (exponent shifts by exact powers of ten). fn parse(s: &str) -> Result { let s = s.trim(); if s.is_empty() || s == "." || s == "-" || s == "-." { return Err(CalcError::Invalid); } // Split off an optional exponent: `1e-15`, `2.5E+3`. let (body, exp) = match s.split_once(['e', 'E']) { Some((b, e)) => { let exp: i32 = e.parse().map_err(|_| CalcError::Invalid)?; (b, exp) } None => (s, 0i32), }; let neg = body.starts_with('-'); let b = if neg { &body[1..] } else { body }; let (int_part, frac_part) = match b.split_once('.') { Some((a, c)) => (a, c), None => (b, ""), }; if int_part.is_empty() && frac_part.is_empty() { return Err(CalcError::Invalid); } let int_part = if int_part.is_empty() { "0" } else { int_part }; let int_val: i128 = int_part.parse().map_err(|_| CalcError::Invalid)?; let frac_digits = frac_part.len(); let frac_val: i128 = if frac_part.is_empty() { 0 } else { frac_part.parse().map_err(|_| CalcError::Invalid)? }; let den = pow10(frac_digits)?; let num = int_val .checked_mul(den) .and_then(|v| v.checked_add(frac_val)) .ok_or(CalcError::Overflow)?; let num = if neg { -num } else { num }; // Apply the exponent with exact power-of-ten arithmetic: // value·10^exp = num·10^exp / 10^frac_digits. let (num, den) = if exp >= 0 { let e = exp as usize; if e >= frac_digits { ( num.checked_mul(pow10(e - frac_digits)?) .ok_or(CalcError::Overflow)?, 1, ) } else { (num, den / pow10(e)?) } } else { ( num, den.checked_mul(pow10((-exp) as usize)?) .ok_or(CalcError::Overflow)?, ) }; Self::new(num, den) } fn to_f64(self) -> f64 { self.num as f64 / self.den as f64 } fn is_integer(self) -> bool { self.den == 1 } fn add(self, o: Self) -> Result { let num = self .num .checked_mul(o.den) .and_then(|a| o.num.checked_mul(self.den).and_then(|b| a.checked_add(b))) .ok_or(CalcError::Overflow)?; let den = self.den.checked_mul(o.den).ok_or(CalcError::Overflow)?; Self::new(num, den) } fn sub(self, o: Self) -> Result { self.add(Rational { num: -o.num, den: o.den, }) } fn mul(self, o: Self) -> Result { let num = self.num.checked_mul(o.num).ok_or(CalcError::Overflow)?; let den = self.den.checked_mul(o.den).ok_or(CalcError::Overflow)?; Self::new(num, den) } fn div(self, o: Self) -> Result { if o.num == 0 { return Err(CalcError::DivByZero); } self.mul(Rational { num: o.den, den: o.num, }) } fn neg(self) -> Self { Self { num: -self.num, den: self.den, } } } /// Numeric value: prefer exact rationals, fall back to float (√ etc.). #[derive(Debug, Clone, Copy, PartialEq)] enum Value { Rat(Rational), Float(f64), } impl Value { fn from_entry(s: &str) -> Result { Ok(Value::Rat(Rational::parse(s)?)) } fn from_f64(v: f64) -> Result { if !v.is_finite() { return Err(CalcError::Overflow); } if v == 0.0 { return Ok(Value::Rat(Rational::from_i128(0))); } // Prefer exact integer when float is (almost) integral. Never snap a // sub-unit magnitude to an integer: that turned 1e-16 into `0`. let r = v.round(); if r != 0.0 && (v - r).abs() < 1e-12 && v.abs() < 1e15 { return Ok(Value::Rat(Rational::from_i128(r as i128))); } Ok(Value::Float(v)) } fn to_f64(self) -> f64 { match self { Value::Rat(r) => r.to_f64(), Value::Float(f) => f, } } fn apply(self, op: Op, other: Self) -> Result { match (self, other) { (Value::Rat(a), Value::Rat(b)) => { let r = match op { Op::Add => a.add(b)?, Op::Sub => a.sub(b)?, Op::Mul => a.mul(b)?, Op::Div => a.div(b)?, }; Ok(Value::Rat(r)) } (a, b) => { let (x, y) = (a.to_f64(), b.to_f64()); let r = match op { Op::Add => x + y, Op::Sub => x - y, Op::Mul => x * y, Op::Div => { if y == 0.0 { return Err(CalcError::DivByZero); } x / y } }; Value::from_f64(r) } } } fn format(self) -> String { match self { Value::Rat(r) => format_rational(r), Value::Float(f) => format_float(f), } } } #[derive(Debug, Clone)] pub struct Calculator { panel: StdPanel, /// Digits currently being entered (or last result as string). entry: String, /// Exact value behind `entry` when not typing (avoids `1/3` → `"0.333…"` → inexact parse). entry_value: Option, /// True while the user is typing a new number. typing: bool, /// Left operand waiting for the next number / equals (standard panel). pending: Option, /// Pending binary operator (standard panel). op: Option, /// Expression line above the main display. expression: String, /// Formula panel: tokens / text of the expression being built (without current entry). formula: String, /// After `=` in formula mode, next digit starts fresh. formula_done: bool, /// Last error, if any. error: Option, /// Memory register. memory: Value, /// 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 { panel: StdPanel::Standard, entry: "0".into(), entry_value: Some(Value::Rat(Rational::from_i128(0))), typing: false, pending: None, op: None, expression: String::new(), formula: String::new(), formula_done: false, error: None, memory: Value::Rat(Rational::from_i128(0)), memory_set: false, } } pub fn panel(&self) -> StdPanel { self.panel } pub fn set_panel(&mut self, panel: StdPanel) { if self.panel == panel { return; } let mem = (self.memory, self.memory_set); *self = Self::new(); self.panel = panel; self.memory = mem.0; self.memory_set = mem.1; } 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) { let panel = self.panel; let memory = self.memory; let memory_set = self.memory_set; *self = Self::new(); self.panel = panel; self.memory = memory; self.memory_set = memory_set; } pub fn clear_entry(&mut self) { self.error = None; self.entry = "0".into(); self.entry_value = Some(Value::Rat(Rational::from_i128(0))); self.typing = false; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } pub fn backspace(&mut self) { if self.error.is_some() { self.clear_entry(); return; } if self.panel == StdPanel::Formula && self.formula_done { return; } if !self.typing { if self.panel == StdPanel::Formula && !self.formula.is_empty() { // Remove trailing operator / open paren from formula. while self.formula.ends_with(' ') { self.formula.pop(); } if let Some(ch) = self.formula.chars().last() { if "++−-×*÷/(".contains(ch) || ch == '−' || ch == '×' || ch == '÷' { self.formula.pop(); while self.formula.ends_with(' ') { self.formula.pop(); } self.sync_formula_expression(); } } } return; } self.entry.pop(); if self.entry.is_empty() || self.entry == "-" { self.entry = "0".into(); self.typing = false; self.entry_value = Some(Value::Rat(Rational::from_i128(0))); } else { self.entry_value = None; } if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } pub fn input_digit(&mut self, d: char) { debug_assert!(d.is_ascii_digit()); if self.error.is_some() { self.clear_all(); } if self.panel == StdPanel::Formula && self.formula_done { self.formula.clear(); self.expression.clear(); self.formula_done = false; self.entry = d.to_string(); self.entry_value = None; self.typing = true; self.sync_formula_expression(); return; } if !self.typing { self.entry = d.to_string(); self.entry_value = None; self.typing = true; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } 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) < 40 && significant_digits(&self.entry) < 16 { self.entry.push(d); } self.entry_value = None; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } pub fn input_dot(&mut self) { if self.error.is_some() { self.clear_all(); } if self.panel == StdPanel::Formula && self.formula_done { self.formula.clear(); self.expression.clear(); self.formula_done = false; self.entry = "0.".into(); self.entry_value = None; self.typing = true; self.sync_formula_expression(); return; } if !self.typing { self.entry = "0.".into(); self.entry_value = None; self.typing = true; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } return; } if !self.entry.contains('.') { self.entry.push('.'); self.entry_value = None; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } } pub fn negate(&mut self) { if self.error.is_some() { return; } if self.panel == StdPanel::Formula && self.formula_done { if let Ok(v) = self.current_value() { self.set_entry_value(v.apply(Op::Mul, Value::Rat(Rational::from_i128(-1))).unwrap_or(v)); } return; } if let Ok(v) = self.current_value() { match v { Value::Rat(r) => self.set_entry_value(Value::Rat(r.neg())), Value::Float(f) => self.set_entry_value(Value::Float(-f)), } self.typing = true; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } } pub fn percent(&mut self) { if self.error.is_some() || self.panel == StdPanel::Formula { return; } let Ok(cur) = self.current_value() else { return; }; let result = match (self.pending, self.op) { (Some(a), Some(Op::Add | Op::Sub)) => a .apply(Op::Mul, cur) .and_then(|p| p.apply(Op::Div, Value::Rat(Rational::from_i128(100)))), _ => cur.apply(Op::Div, Value::Rat(Rational::from_i128(100))), }; match result { Ok(v) => { self.set_entry_value(v); self.typing = false; } Err(e) => self.fail(e), } } pub fn reciprocal(&mut self) { self.unary(|x| Value::Rat(Rational::from_i128(1)).apply(Op::Div, x)); } pub fn square(&mut self) { self.unary(|x| x.apply(Op::Mul, x)); } pub fn sqrt(&mut self) { self.unary(|x| { let f = x.to_f64(); if f < 0.0 { Err(CalcError::Invalid) } else { Value::from_f64(f.sqrt()) } }); } pub fn set_op(&mut self, op: Op) { if self.error.is_some() { return; } match self.panel { StdPanel::Standard => self.set_op_standard(op), StdPanel::Formula => self.set_op_formula(op), } } pub fn paren_open(&mut self) { if self.panel != StdPanel::Formula || self.error.is_some() { return; } if self.formula_done { self.formula.clear(); self.formula_done = false; self.entry = "0".into(); self.typing = false; } // If a number was being typed, imply multiply: 2(3+1) if self.typing { self.flush_entry_to_formula(); if !self.formula.is_empty() && !self.formula.ends_with('(') && !self.formula.ends_with(' ') { // already flushed number; add × before ( // flush_entry_to_formula already appended the number } let trimmed = self.formula.trim_end(); if trimmed .chars() .last() .is_some_and(|c| c.is_ascii_digit() || c == ')') { self.formula.push_str(" × "); } } else if !self.formula.is_empty() { let trimmed = self.formula.trim_end(); if trimmed .chars() .last() .is_some_and(|c| c.is_ascii_digit() || c == ')') { self.formula.push_str(" × "); } } self.formula.push('('); self.entry = "0".into(); self.typing = false; self.sync_formula_expression(); } pub fn paren_close(&mut self) { if self.panel != StdPanel::Formula || self.error.is_some() { return; } if self.formula_done { return; } let opens = self.formula.chars().filter(|&c| c == '(').count(); let closes = self.formula.chars().filter(|&c| c == ')').count(); if opens <= closes { return; } if self.typing || self.entry != "0" || !self.formula.ends_with('(') { self.flush_entry_to_formula(); } self.formula.push(')'); self.entry = "0".into(); self.typing = false; self.sync_formula_expression(); } pub fn equals(&mut self) { if self.error.is_some() { return; } match self.panel { StdPanel::Standard => self.equals_standard(), StdPanel::Formula => self.equals_formula(), } } // --- Memory --- pub fn memory_clear(&mut self) { self.memory = Value::Rat(Rational::from_i128(0)); self.memory_set = false; } pub fn memory_recall(&mut self) { if self.error.is_some() { self.clear_all(); } if self.panel == StdPanel::Formula && self.formula_done { self.formula.clear(); self.formula_done = false; } self.set_entry_value(self.memory); self.typing = false; if self.panel == StdPanel::Formula { self.sync_formula_expression(); } } pub fn memory_add(&mut self) { if self.error.is_some() { return; } if let Ok(v) = self.current_value() { match self.memory.apply(Op::Add, v) { Ok(m) => { self.memory = m; self.memory_set = true; self.typing = false; } Err(e) => self.fail(e), } } } pub fn memory_sub(&mut self) { if self.error.is_some() { return; } if let Ok(v) = self.current_value() { match self.memory.apply(Op::Sub, v) { Ok(m) => { self.memory = m; self.memory_set = true; self.typing = false; } Err(e) => self.fail(e), } } } 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; } } // --- standard panel --- fn set_op_standard(&mut self, op: Op) { 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!("{} {}", v.format(), op.symbol()); self.typing = false; } fn equals_standard(&mut self) { 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 a.apply(op, b) { Ok(r) => { self.expression = format!( "{} {} {} =", a.format(), op.symbol(), b.format() ); self.set_entry_value(r); self.pending = None; self.op = None; 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 = a.apply(op, b)?; self.set_entry_value(r); self.pending = Some(r); self.op = None; self.typing = false; Ok(()) } // --- formula panel --- fn set_op_formula(&mut self, op: Op) { if self.formula_done { // Continue from previous result: `ans + …` self.formula.clear(); self.formula_done = false; self.typing = true; // treat current entry as left operand } self.flush_entry_to_formula(); // Replace trailing operator if user changes mind: `1 +` then `-` → `1 −` let trimmed = self.formula.trim_end(); if let Some(last) = trimmed.chars().last() { if matches!(last, '+' | '−' | '×' | '÷' | '-' | '*' | '/') { while self.formula.ends_with(' ') { self.formula.pop(); } self.formula.pop(); while self.formula.ends_with(' ') { self.formula.pop(); } } } if !self.formula.is_empty() { self.formula.push(' '); } self.formula.push_str(op.symbol()); self.formula.push(' '); self.entry = "0".into(); self.typing = false; self.sync_formula_expression(); } fn equals_formula(&mut self) { let mut src = self.formula.clone(); let ends_with_paren = src.trim_end().ends_with(')'); if !ends_with_paren { if !src.is_empty() && !src.ends_with(' ') && !src.ends_with('(') { src.push(' '); } src.push_str(&self.entry); } else if src.is_empty() { src = self.entry.clone(); } // Auto-close parentheses. let opens = src.chars().filter(|&c| c == '(').count(); let closes = src.chars().filter(|&c| c == ')').count(); for _ in 0..opens.saturating_sub(closes) { src.push(')'); } match eval_expression(&src) { Ok(v) => { self.expression = format!("{} =", src); self.set_entry_value(v); self.formula.clear(); self.typing = false; self.formula_done = true; } Err(e) => self.fail(e), } } fn flush_entry_to_formula(&mut self) { if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with(')') { return; } if !self.formula.is_empty() && !self.formula.ends_with(' ') && !self.formula.ends_with('(') { self.formula.push(' '); } self.formula.push_str(&self.entry); self.typing = false; } fn sync_formula_expression(&mut self) { if self.formula_done { return; } let mut s = self.formula.clone(); if self.typing { if !s.is_empty() && !s.ends_with(' ') && !s.ends_with('(') { s.push(' '); } s.push_str(&self.entry); } self.expression = s; } // --- internals --- fn unary(&mut self, f: impl FnOnce(Value) -> Result) { if self.error.is_some() { return; } let Ok(v) = self.current_value() else { return; }; match f(v) { Ok(r) => { if self.panel == StdPanel::Formula { // Unary applies to current entry only. self.set_entry_value(r); self.typing = true; self.formula_done = false; self.sync_formula_expression(); } else { self.expression.clear(); self.set_entry_value(r); self.typing = false; } } Err(e) => self.fail(e), } } fn current_value(&self) -> Result { if !self.typing { if let Some(v) = self.entry_value { return Ok(v); } } Value::from_entry(&self.entry) } fn set_entry_value(&mut self, v: Value) { self.entry = v.format(); self.entry_value = Some(v); self.error = None; } fn fail(&mut self, e: CalcError) { self.error = Some(e); self.pending = None; self.op = None; self.entry_value = None; self.expression.clear(); self.formula.clear(); self.formula_done = false; self.typing = false; } } // --- expression parser (formula mode) --- #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Tok { Num, // placeholder — numbers carried separately Op(Op), LParen, RParen, } fn eval_expression(src: &str) -> Result { let tokens = tokenize(src)?; let mut idx = 0; let value = parse_expr(&tokens, &mut idx)?; if idx != tokens.len() { return Err(CalcError::Invalid); } Ok(value) } fn tokenize(src: &str) -> Result)>, CalcError> { let mut out = Vec::new(); let chars: Vec = src.chars().collect(); let mut i = 0; while i < chars.len() { let c = chars[i]; if c.is_whitespace() { i += 1; continue; } match c { '+' => { out.push((Tok::Op(Op::Add), None)); i += 1; } '−' | '-' => { out.push((Tok::Op(Op::Sub), None)); i += 1; } '×' | '*' => { out.push((Tok::Op(Op::Mul), None)); i += 1; } '÷' | '/' => { out.push((Tok::Op(Op::Div), None)); i += 1; } '(' => { out.push((Tok::LParen, None)); i += 1; } ')' => { out.push((Tok::RParen, None)); i += 1; } '0'..='9' | '.' => { let start = i; i += 1; while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') { i += 1; } // Optional decimal exponent: `1e-9`, `2.5E+3`. if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') { let mut j = i + 1; if j < chars.len() && (chars[j] == '+' || chars[j] == '-') { j += 1; } if j < chars.len() && chars[j].is_ascii_digit() { i = j; while i < chars.len() && chars[i].is_ascii_digit() { i += 1; } } } let lit: String = chars[start..i].iter().collect(); let v = Value::from_entry(&lit)?; out.push((Tok::Num, Some(v))); } _ => return Err(CalcError::Invalid), } } Ok(out) } fn parse_expr(tokens: &[(Tok, Option)], idx: &mut usize) -> Result { let mut left = parse_term(tokens, idx)?; while let Some((Tok::Op(op @ (Op::Add | Op::Sub)), _)) = tokens.get(*idx).copied() { *idx += 1; let right = parse_term(tokens, idx)?; left = left.apply(op, right)?; } Ok(left) } fn parse_term(tokens: &[(Tok, Option)], idx: &mut usize) -> Result { let mut left = parse_unary(tokens, idx)?; while let Some((Tok::Op(op @ (Op::Mul | Op::Div)), _)) = tokens.get(*idx).copied() { *idx += 1; let right = parse_unary(tokens, idx)?; left = left.apply(op, right)?; } Ok(left) } fn parse_unary(tokens: &[(Tok, Option)], idx: &mut usize) -> Result { if let Some((Tok::Op(Op::Sub), _)) = tokens.get(*idx).copied() { *idx += 1; let v = parse_unary(tokens, idx)?; return v.apply(Op::Mul, Value::Rat(Rational::from_i128(-1))); } if let Some((Tok::Op(Op::Add), _)) = tokens.get(*idx).copied() { *idx += 1; return parse_unary(tokens, idx); } parse_primary(tokens, idx) } fn parse_primary(tokens: &[(Tok, Option)], idx: &mut usize) -> Result { let Some((tok, val)) = tokens.get(*idx).copied() else { return Err(CalcError::Invalid); }; match tok { Tok::Num => { *idx += 1; val.ok_or(CalcError::Invalid) } Tok::LParen => { *idx += 1; let v = parse_expr(tokens, idx)?; match tokens.get(*idx).copied() { Some((Tok::RParen, _)) => { *idx += 1; Ok(v) } _ => Err(CalcError::Invalid), } } _ => Err(CalcError::Invalid), } } // --- formatting / helpers --- fn digit_count(s: &str) -> usize { s.chars().filter(|c| c.is_ascii_digit()).count() } /// Significant digits (leading zeros don't count) — caps precision, not /// magnitude, so `0.0000000000000001` (1e-16) can be typed. fn significant_digits(s: &str) -> usize { s.chars() .filter(|c| c.is_ascii_digit()) .skip_while(|&c| c == '0') .count() } fn gcd(mut a: u128, mut b: u128) -> u128 { while b != 0 { let t = b; b = a % b; a = t; } a } fn pow10(n: usize) -> Result { let mut r: i128 = 1; for _ in 0..n { r = r.checked_mul(10).ok_or(CalcError::Overflow)?; } Ok(r) } fn format_rational(r: Rational) -> String { if r.num == 0 { return "0".into(); } if r.is_integer() { return r.num.to_string(); } // Exact terminating decimal if den's primes ⊆ {2,5}. if let Some(s) = terminating_decimal(r) { return s; } format_float(r.to_f64()) } fn terminating_decimal(r: Rational) -> Option { // Terminating iff den = 2^a · 5^b. Digit count after the point is // k = max(a, b); scale num so the denominator becomes exactly 10^k. let mut den = r.den; let (mut a, mut b) = (0u32, 0u32); while den % 2 == 0 { den /= 2; a += 1; } while den % 5 == 0 { den /= 5; b += 1; } if den != 1 { return None; } let k = a.max(b); let mut n = r.num.abs(); for _ in 0..(k - a) { n = n.checked_mul(2)?; } for _ in 0..(k - b) { n = n.checked_mul(5)?; } let mut d: i128 = 1; for _ in 0..k { d = d.checked_mul(10)?; } let int_part = n / d; let mut frac = (n % d).to_string(); while frac.len() < k as usize { frac.insert(0, '0'); } // trim trailing zeros while frac.ends_with('0') { frac.pop(); } let body = if frac.is_empty() { int_part.to_string() } else { format!("{int_part}.{frac}") }; Some(if r.num < 0 { format!("-{body}") } else { body }) } fn format_float(v: f64) -> String { if !v.is_finite() || v == 0.0 { return "0".into(); } // Snap near-integers (guards any float path residue); never for |v| < 1, // otherwise tiny results like 1e-16 collapse to `0`. let r = v.round(); if r != 0.0 && (v - r).abs() < 1e-10 && v.abs() < 1e15 { return format!("{}", r as i64); } // Very small / large magnitudes: scientific notation keeps the value // visible and re-parseable (`Rational::parse` understands exponents). if v.abs() < 1e-10 || v.abs() >= 1e16 { return format!("{v:e}"); } // Shortest representation that round-trips back to `v` exactly. format!("{v}") } impl fmt::Display for Rational { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.den == 1 { write!(f, "{}", self.num) } else { write!(f, "{}/{}", self.num, self.den) } } } #[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 one_third_times_three_standard() { let mut c = Calculator::new(); c.input_digit('1'); c.set_op(Op::Div); c.input_digit('3'); c.set_op(Op::Mul); c.input_digit('3'); c.equals(); assert_eq!(c.display(), "1"); } #[test] fn one_third_times_three_formula() { let mut c = Calculator::new(); c.set_panel(StdPanel::Formula); c.input_digit('1'); c.set_op(Op::Div); c.input_digit('3'); c.set_op(Op::Mul); c.input_digit('3'); c.equals(); assert_eq!(c.display(), "1"); assert!(c.expression().contains('=')); } #[test] fn formula_precedence() { let mut c = Calculator::new(); c.set_panel(StdPanel::Formula); c.input_digit('1'); c.set_op(Op::Add); c.input_digit('2'); c.set_op(Op::Mul); c.input_digit('3'); c.equals(); assert_eq!(c.display(), "7"); } #[test] fn formula_parens() { let mut c = Calculator::new(); c.set_panel(StdPanel::Formula); c.paren_open(); c.input_digit('1'); c.set_op(Op::Add); c.input_digit('2'); c.paren_close(); c.set_op(Op::Mul); c.input_digit('3'); c.equals(); assert_eq!(c.display(), "9"); } #[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")); } #[test] fn rational_parse_decimal() { let r = Rational::parse("0.5").unwrap(); assert_eq!(r, Rational::new(1, 2).unwrap()); let r = Rational::parse("1.25").unwrap(); assert_eq!(r, Rational::new(5, 4).unwrap()); } // --- regression tests (rounding / formatting bugs) --- #[test] fn quarter_and_eighth_display() { // terminating_decimal scaled the fraction wrong: 1/4 displayed as "0.5". let mut c = Calculator::new(); c.input_digit('1'); c.set_op(Op::Div); c.input_digit('4'); c.equals(); assert_eq!(c.display(), "0.25"); c.clear_all(); c.input_digit('1'); c.set_op(Op::Div); c.input_digit('8'); c.equals(); assert_eq!(c.display(), "0.125"); } #[test] fn five_hundredths_display() { // 0.05 must survive exact formatting (was displayed as "0.1"). let mut c = Calculator::new(); c.input_digit('0'); c.input_dot(); c.input_digit('0'); c.input_digit('5'); // 0.05 c.set_op(Op::Mul); c.input_digit('1'); c.equals(); assert_eq!(c.display(), "0.05"); } #[test] fn tiny_reciprocal_not_zero() { // 1/9999999999999999 ≈ 1e-16 was snapped to "0" by format_float. let mut c = Calculator::new(); for d in "9999999999999999".chars() { c.input_digit(d); } c.reciprocal(); let disp = c.display(); assert!(disp != "0", "tiny reciprocal must not display as 0, got {disp}"); } #[test] fn tiny_result_reusable_standard() { // Float result in scientific notation must be re-parsable as operand. let mut c = Calculator::new(); for d in "9999999999999999".chars() { c.input_digit(d); } c.reciprocal(); c.set_op(Op::Mul); c.input_digit('2'); c.equals(); let disp = c.display(); assert!(disp != "0" && disp != "Invalid input", "got {disp}"); } #[test] fn sqrt_square_roundtrip_tiny() { // √(1e-30) = 1e-15 was snapped to 0 by Value::from_f64. let mut c = Calculator::new(); c.input_dot(); // "0." for _ in 0..14 { c.input_digit('0'); } c.input_digit('1'); // 0.000000000000001 = 1e-15 c.square(); c.sqrt(); let disp = c.display(); assert!(disp != "0", "sqrt(1e-30) must not display as 0, got {disp}"); } #[test] fn rational_parse_exponent() { // Scientific-notation entries must parse to exact fractions. let r = Rational::parse("1e-15").unwrap(); assert_eq!(r, Rational::new(1, 1_000_000_000_000_000).unwrap()); let r = Rational::parse("1.5e3").unwrap(); assert_eq!(r, Rational::new(1500, 1).unwrap()); let r = Rational::parse("2.5e-2").unwrap(); assert_eq!(r, Rational::new(1, 40).unwrap()); let r = Rational::parse("-1E+3").unwrap(); assert_eq!(r, Rational::new(-1000, 1).unwrap()); assert!(Rational::parse("1e2e3").is_err()); assert!(Rational::parse("1e").is_err()); } #[test] fn can_type_one_e_minus_16() { // Leading zeros must not eat the 16-significant-digit budget. let mut c = Calculator::new(); c.input_dot(); for _ in 0..15 { c.input_digit('0'); } c.input_digit('1'); assert_eq!(c.display(), "0.0000000000000001"); } }