Skip to main content

calculator/
engine.rs

1//! What a pocket calculator does, without any interface. Operations run in the order they
2//! are pressed, so `2 + 3 × 4 =` is 20.
3
4/// Digits the display takes while typing and when showing a result.
5const DIGITS: usize = 12;
6
7/// A key of the calculator.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Press {
10    /// A digit from 0 to 9.
11    Digit(u8),
12    /// The decimal point.
13    Point,
14    /// An operation between two numbers.
15    Op(Op),
16    /// Runs the pending operation; pressed again, repeats it.
17    Equals,
18    /// Turns the number on display into a percentage.
19    Percent,
20    /// Changes the sign of the number on display.
21    Negate,
22    /// Erases the last digit typed.
23    Backspace,
24    /// Starts over.
25    Clear,
26}
27
28/// An operation between two numbers.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Op {
31    /// Addition.
32    Add,
33    /// Subtraction.
34    Subtract,
35    /// Multiplication.
36    Multiply,
37    /// Division.
38    Divide,
39}
40
41impl Op {
42    fn apply(self, a: f64, b: f64) -> Option<f64> {
43        let result = match self {
44            Self::Add => a + b,
45            Self::Subtract => a - b,
46            Self::Multiply => a * b,
47            Self::Divide if b == 0.0 => return None,
48            Self::Divide => a / b,
49        };
50        result.is_finite().then_some(result)
51    }
52}
53
54/// The calculator's memory.
55#[derive(Clone, Debug, PartialEq)]
56pub struct Engine {
57    /// What the display shows.
58    display: String,
59    /// The number before the pending operation.
60    stored: f64,
61    pending: Option<Op>,
62    /// The last operation and its right side, for `=` to repeat.
63    last: Option<(Op, f64)>,
64    /// The next digit starts a new number.
65    fresh: bool,
66    failed: bool,
67}
68
69impl Default for Engine {
70    fn default() -> Self {
71        Self { display: "0".into(), stored: 0.0, pending: None, last: None, fresh: true, failed: false }
72    }
73}
74
75impl Engine {
76    /// What the display shows.
77    pub fn display(&self) -> &str {
78        if self.failed { "Error" } else { &self.display }
79    }
80
81    /// The operation waiting for its right side.
82    pub fn pending(&self) -> Option<Op> {
83        self.pending
84    }
85
86    /// Takes a key.
87    pub fn press(&mut self, press: Press) {
88        if self.failed {
89            // After an error, only something that starts a new number does anything.
90            if !matches!(press, Press::Digit(_) | Press::Point | Press::Clear) {
91                return;
92            }
93            *self = Self::default();
94        }
95        match press {
96            Press::Digit(digit) => self.digit(digit),
97            Press::Point => self.point(),
98            Press::Op(op) => self.operation(op),
99            Press::Equals => self.equals(),
100            Press::Percent => self.percent(),
101            Press::Negate => self.negate(),
102            Press::Backspace => self.backspace(),
103            Press::Clear => *self = Self::default(),
104        }
105    }
106
107    fn value(&self) -> f64 {
108        self.display.parse().unwrap_or(0.0)
109    }
110
111    fn digit(&mut self, digit: u8) {
112        let digit = char::from(b'0' + digit.min(9));
113        if self.fresh {
114            self.display = digit.to_string();
115            self.fresh = false;
116        } else if self.display == "0" {
117            self.display = digit.to_string();
118        } else if self.display == "-0" {
119            self.display = format!("-{digit}");
120        } else if self.display.chars().filter(char::is_ascii_digit).count() < DIGITS {
121            self.display.push(digit);
122        }
123    }
124
125    fn point(&mut self) {
126        if self.fresh {
127            self.display = "0.".into();
128            self.fresh = false;
129        } else if !self.display.contains('.') {
130            self.display.push('.');
131        }
132    }
133
134    fn operation(&mut self, op: Op) {
135        // A second operation in a row only replaces the first.
136        if self.pending.is_some() && !self.fresh {
137            self.run_pending();
138            if self.failed {
139                return;
140            }
141        } else if self.pending.is_none() {
142            self.stored = self.value();
143        }
144        self.pending = Some(op);
145        self.fresh = true;
146    }
147
148    fn run_pending(&mut self) {
149        if let Some(op) = self.pending.take() {
150            let right = self.value();
151            self.last = Some((op, right));
152            self.finish(op.apply(self.stored, right));
153        }
154    }
155
156    fn equals(&mut self) {
157        if self.pending.is_some() {
158            // `5 + =` adds the 5 to itself, as pocket calculators do.
159            self.run_pending();
160        } else if let Some((op, right)) = self.last {
161            self.finish(op.apply(self.value(), right));
162        }
163        self.fresh = true;
164    }
165
166    fn finish(&mut self, result: Option<f64>) {
167        match result {
168            Some(result) => {
169                self.stored = result;
170                self.display = format_number(result);
171            }
172            None => self.failed = true,
173        }
174        self.fresh = true;
175    }
176
177    fn percent(&mut self) {
178        let shown = self.value();
179        // `200 + 10 %` is 10% of 200; `200 × 10 %` is 10% as a fraction.
180        let result = match self.pending {
181            Some(Op::Add | Op::Subtract) => self.stored * shown / 100.0,
182            _ => shown / 100.0,
183        };
184        self.display = format_number(result);
185        self.fresh = false;
186    }
187
188    fn negate(&mut self) {
189        match self.display.strip_prefix('-') {
190            Some(rest) => self.display = rest.to_owned(),
191            None => self.display.insert(0, '-'),
192        }
193        if self.pending.is_none() && self.fresh {
194            self.stored = self.value();
195        }
196    }
197
198    fn backspace(&mut self) {
199        if self.fresh {
200            return;
201        }
202        self.display.pop();
203        if self.display.is_empty() || self.display == "-" {
204            self.display = "0".into();
205        }
206    }
207}
208
209/// Shows a number the way a person expects: without the noise of binary fractions, so
210/// `0.1 + 0.2` is `0.3`, and in scientific notation when it does not fit.
211pub fn format_number(value: f64) -> String {
212    if value == 0.0 {
213        return "0".into();
214    }
215    let size = value.abs().log10().floor();
216    if !(-6.0..DIGITS as f64).contains(&size) {
217        let text = format!("{:.*e}", DIGITS - 6, value);
218        let (mantissa, exponent) = text.split_once('e').unwrap_or((&text, "0"));
219        return format!("{}e{exponent}", trim_zeros(mantissa));
220    }
221    let decimals = (DIGITS as f64 - 1.0 - size).max(0.0) as usize;
222    trim_zeros(&format!("{value:.decimals$}")).to_owned()
223}
224
225fn trim_zeros(text: &str) -> &str {
226    if text.contains('.') { text.trim_end_matches('0').trim_end_matches('.') } else { text }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    /// Presses keys written as text, and says what the display shows.
234    fn run(keys: &str) -> String {
235        let mut engine = Engine::default();
236        for key in keys.chars() {
237            engine.press(match key {
238                '0'..='9' => Press::Digit(key as u8 - b'0'),
239                '.' => Press::Point,
240                '+' => Press::Op(Op::Add),
241                '-' => Press::Op(Op::Subtract),
242                '*' => Press::Op(Op::Multiply),
243                '/' => Press::Op(Op::Divide),
244                '=' => Press::Equals,
245                '%' => Press::Percent,
246                'n' => Press::Negate,
247                'b' => Press::Backspace,
248                'c' => Press::Clear,
249                other => panic!("no key {other}"),
250            });
251        }
252        engine.display().to_owned()
253    }
254
255    #[test]
256    fn does_the_four_operations() {
257        assert_eq!(run("2+3="), "5");
258        assert_eq!(run("9-12="), "-3");
259        assert_eq!(run("6*7="), "42");
260        assert_eq!(run("1/4="), "0.25");
261    }
262
263    #[test]
264    fn runs_in_the_order_pressed() {
265        assert_eq!(run("2+3*4="), "20");
266    }
267
268    #[test]
269    fn hides_the_noise_of_binary_fractions() {
270        assert_eq!(run("0.1+0.2="), "0.3");
271        assert_eq!(run("1/3="), "0.333333333333");
272    }
273
274    #[test]
275    fn dividing_by_zero_is_an_error_until_a_new_number() {
276        assert_eq!(run("5/0="), "Error");
277        assert_eq!(run("5/0=+"), "Error");
278        assert_eq!(run("5/0=7"), "7");
279        assert_eq!(run("5/0=c"), "0");
280    }
281
282    #[test]
283    fn equals_again_repeats_the_operation() {
284        assert_eq!(run("2+3=="), "8");
285        assert_eq!(run("2+3==="), "11");
286    }
287
288    #[test]
289    fn a_second_operation_replaces_the_first() {
290        assert_eq!(run("8+-2="), "6");
291    }
292
293    #[test]
294    fn a_result_is_the_start_of_the_next_operation() {
295        assert_eq!(run("2+3=*2="), "10");
296    }
297
298    #[test]
299    fn a_digit_after_a_result_starts_over() {
300        assert_eq!(run("2+3=7"), "7");
301        assert_eq!(run("2+3=7+1="), "8");
302    }
303
304    #[test]
305    fn typing_keeps_to_one_point_and_twelve_digits() {
306        assert_eq!(run("1.2.3"), "1.23");
307        assert_eq!(run(".5"), "0.5");
308        assert_eq!(run("007"), "7");
309        assert_eq!(run("12345678901234"), "123456789012");
310    }
311
312    #[test]
313    fn percent_is_of_the_left_side_when_adding() {
314        assert_eq!(run("200+10%="), "220");
315        assert_eq!(run("200-10%="), "180");
316        assert_eq!(run("200*10%="), "20");
317        assert_eq!(run("50%"), "0.5");
318    }
319
320    #[test]
321    fn negate_and_backspace_edit_what_is_shown() {
322        assert_eq!(run("5n"), "-5");
323        assert_eq!(run("5nn"), "5");
324        assert_eq!(run("3n+5="), "2");
325        assert_eq!(run("123b"), "12");
326        assert_eq!(run("1b"), "0");
327        assert_eq!(run("5nb"), "0");
328        assert_eq!(run("2+3=b"), "5");
329    }
330
331    #[test]
332    fn numbers_out_of_range_go_scientific_or_fail() {
333        assert_eq!(run("999999999999*999999999999="), "1e24");
334        assert_eq!(run("1/10000000="), "1e-7");
335        assert_eq!(format_number(f64::MAX), "1.797693e308");
336        let mut engine = Engine {
337            stored: f64::MAX,
338            pending: Some(Op::Multiply),
339            display: "10".into(),
340            fresh: false,
341            ..Engine::default()
342        };
343        engine.press(Press::Equals);
344        assert_eq!(engine.display(), "Error");
345    }
346}