1const DIGITS: usize = 12;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Press {
10 Digit(u8),
12 Point,
14 Op(Op),
16 Equals,
18 Percent,
20 Negate,
22 Backspace,
24 Clear,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Op {
31 Add,
33 Subtract,
35 Multiply,
37 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#[derive(Clone, Debug, PartialEq)]
56pub struct Engine {
57 display: String,
59 stored: f64,
61 pending: Option<Op>,
62 last: Option<(Op, f64)>,
64 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 pub fn display(&self) -> &str {
78 if self.failed { "Error" } else { &self.display }
79 }
80
81 pub fn pending(&self) -> Option<Op> {
83 self.pending
84 }
85
86 pub fn press(&mut self, press: Press) {
88 if self.failed {
89 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 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 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 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
209pub 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 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}