1339 lines
38 KiB
Rust
1339 lines
38 KiB
Rust
//! 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<Self, CalcError> {
|
||
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<Self, CalcError> {
|
||
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<Self, CalcError> {
|
||
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, CalcError> {
|
||
self.add(Rational {
|
||
num: -o.num,
|
||
den: o.den,
|
||
})
|
||
}
|
||
|
||
fn mul(self, o: Self) -> Result<Self, CalcError> {
|
||
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<Self, CalcError> {
|
||
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<Self, CalcError> {
|
||
Ok(Value::Rat(Rational::parse(s)?))
|
||
}
|
||
|
||
fn from_f64(v: f64) -> Result<Self, CalcError> {
|
||
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<Self, CalcError> {
|
||
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<Value>,
|
||
/// True while the user is typing a new number.
|
||
typing: bool,
|
||
/// Left operand waiting for the next number / equals (standard panel).
|
||
pending: Option<Value>,
|
||
/// Pending binary operator (standard panel).
|
||
op: Option<Op>,
|
||
/// 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<CalcError>,
|
||
/// 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<Value, CalcError>) {
|
||
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<Value, CalcError> {
|
||
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<Value, CalcError> {
|
||
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<Vec<(Tok, Option<Value>)>, CalcError> {
|
||
let mut out = Vec::new();
|
||
let chars: Vec<char> = 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<Value>)], idx: &mut usize) -> Result<Value, CalcError> {
|
||
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<Value>)], idx: &mut usize) -> Result<Value, CalcError> {
|
||
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<Value>)], idx: &mut usize) -> Result<Value, CalcError> {
|
||
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<Value>)], idx: &mut usize) -> Result<Value, CalcError> {
|
||
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<i128, CalcError> {
|
||
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<String> {
|
||
// 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");
|
||
}
|
||
}
|