Files
RCalc/src/engine.rs
T
2026-08-30 12:22:00 +03:00

1339 lines
38 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 / 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");
}
}