aprender_test_showcase/tui/
app.rs1use crate::core::evaluator::Evaluator;
6use crate::core::history::History;
7use crate::core::{AnomalyValidator, CalcError};
8
9#[derive(Debug)]
11pub struct CalculatorApp {
12 input: String,
14 cursor: usize,
16 result: Option<Result<f64, CalcError>>,
18 history: History,
20 evaluator: Evaluator,
22 should_quit: bool,
24}
25
26impl Default for CalculatorApp {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl CalculatorApp {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 input: String::new(),
38 cursor: 0,
39 result: None,
40 history: History::new(),
41 evaluator: Evaluator::new(),
42 should_quit: false,
43 }
44 }
45
46 #[must_use]
48 pub fn with_validator(validator: AnomalyValidator) -> Self {
49 Self {
50 input: String::new(),
51 cursor: 0,
52 result: None,
53 history: History::new(),
54 evaluator: Evaluator::with_validator(validator),
55 should_quit: false,
56 }
57 }
58
59 #[must_use]
61 pub fn input(&self) -> &str {
62 &self.input
63 }
64
65 #[must_use]
67 pub fn cursor(&self) -> usize {
68 self.cursor
69 }
70
71 #[must_use]
73 pub fn result(&self) -> Option<&Result<f64, CalcError>> {
74 self.result.as_ref()
75 }
76
77 #[must_use]
79 pub fn history(&self) -> &History {
80 &self.history
81 }
82
83 #[must_use]
85 pub fn should_quit(&self) -> bool {
86 self.should_quit
87 }
88
89 pub fn quit(&mut self) {
91 self.should_quit = true;
92 }
93
94 pub fn set_input(&mut self, input: &str) {
96 self.input = input.to_string();
97 self.cursor = self.input.len();
98 }
99
100 pub fn set_cursor(&mut self, pos: usize) {
102 self.cursor = pos.min(self.input.len());
103 }
104
105 pub fn insert_char(&mut self, c: char) {
107 self.input.insert(self.cursor, c);
108 self.cursor += 1;
109 }
110
111 pub fn delete_char(&mut self) {
113 if self.cursor > 0 {
114 self.cursor -= 1;
115 self.input.remove(self.cursor);
116 }
117 }
118
119 pub fn delete_char_forward(&mut self) {
121 if self.cursor < self.input.len() {
122 self.input.remove(self.cursor);
123 }
124 }
125
126 pub fn move_cursor_left(&mut self) {
128 if self.cursor > 0 {
129 self.cursor -= 1;
130 }
131 }
132
133 pub fn move_cursor_right(&mut self) {
135 if self.cursor < self.input.len() {
136 self.cursor += 1;
137 }
138 }
139
140 pub fn move_cursor_start(&mut self) {
142 self.cursor = 0;
143 }
144
145 pub fn move_cursor_end(&mut self) {
147 self.cursor = self.input.len();
148 }
149
150 pub fn clear(&mut self) {
152 self.input.clear();
153 self.cursor = 0;
154 self.result = None;
155 }
156
157 pub fn clear_all(&mut self) {
159 self.clear();
160 self.history.clear();
161 }
162
163 pub fn evaluate(&mut self) {
165 if self.input.is_empty() {
166 return;
167 }
168
169 let result = self.evaluator.evaluate_str(&self.input);
170
171 if let Ok(value) = &result {
173 self.history.record(&self.input, *value);
174 }
175
176 self.result = Some(result);
177 }
178
179 #[must_use]
181 pub fn result_display(&self) -> String {
182 match &self.result {
183 None => String::new(),
184 Some(Ok(value)) => format_result(*value),
185 Some(Err(e)) => format!("Error: {e}"),
186 }
187 }
188
189 pub fn recall_last(&mut self) {
191 if let Some(entry) = self.history.last() {
192 self.input = entry.expression.clone();
193 self.cursor = self.input.len();
194 }
195 }
196
197 #[must_use]
199 pub fn jidoka_status(&self) -> Vec<String> {
200 let mut status = Vec::new();
201
202 match &self.result {
203 None => {
204 status.push("Ready".into());
205 }
206 Some(Ok(_)) => {
207 status.push("✓ No NaN detected".into());
208 status.push("✓ No overflow".into());
209 status.push("✓ All invariants satisfied".into());
210 }
211 Some(Err(CalcError::AnomalyViolation(v))) => {
212 status.push(format!("✗ Anomaly violation: {v}"));
213 }
214 Some(Err(e)) => {
215 status.push(format!("✗ Error: {e}"));
216 }
217 }
218
219 status
220 }
221}
222
223fn format_result(value: f64) -> String {
225 if value.fract() == 0.0 && value.abs() < 1e15 {
226 format!("{:.0}", value)
227 } else {
228 let formatted = format!("{:.10}", value);
230 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
232 trimmed.to_string()
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
243 fn test_app_new() {
244 let app = CalculatorApp::new();
245 assert!(app.input().is_empty());
246 assert_eq!(app.cursor(), 0);
247 assert!(app.result().is_none());
248 assert!(app.history().is_empty());
249 assert!(!app.should_quit());
250 }
251
252 #[test]
253 fn test_app_default() {
254 let app = CalculatorApp::default();
255 assert!(app.input().is_empty());
256 }
257
258 #[test]
259 fn test_app_with_validator() {
260 let validator = AnomalyValidator::with_max_magnitude(100.0);
261 let mut app = CalculatorApp::with_validator(validator);
262 app.set_input("50 * 3");
263 app.evaluate();
264 assert!(matches!(
265 app.result(),
266 Some(Err(CalcError::AnomalyViolation(_)))
267 ));
268 }
269
270 #[test]
273 fn test_set_input() {
274 let mut app = CalculatorApp::new();
275 app.set_input("2 + 2");
276 assert_eq!(app.input(), "2 + 2");
277 assert_eq!(app.cursor(), 5);
278 }
279
280 #[test]
281 fn test_insert_char() {
282 let mut app = CalculatorApp::new();
283 app.insert_char('1');
284 app.insert_char('+');
285 app.insert_char('2');
286 assert_eq!(app.input(), "1+2");
287 assert_eq!(app.cursor(), 3);
288 }
289
290 #[test]
291 fn test_insert_char_in_middle() {
292 let mut app = CalculatorApp::new();
293 app.set_input("12");
294 app.set_cursor(1); app.insert_char('+');
296 assert_eq!(app.input(), "1+2");
297 }
298
299 #[test]
300 fn test_delete_char() {
301 let mut app = CalculatorApp::new();
302 app.set_input("123");
303 app.delete_char();
304 assert_eq!(app.input(), "12");
305 assert_eq!(app.cursor(), 2);
306 }
307
308 #[test]
309 fn test_delete_char_at_start() {
310 let mut app = CalculatorApp::new();
311 app.set_input("123");
312 app.set_cursor(0);
313 app.delete_char(); assert_eq!(app.input(), "123");
315 }
316
317 #[test]
318 fn test_delete_char_forward() {
319 let mut app = CalculatorApp::new();
320 app.set_input("123");
321 app.set_cursor(0);
322 app.delete_char_forward();
323 assert_eq!(app.input(), "23");
324 }
325
326 #[test]
327 fn test_delete_char_forward_at_end() {
328 let mut app = CalculatorApp::new();
329 app.set_input("123");
330 app.delete_char_forward(); assert_eq!(app.input(), "123");
332 }
333
334 #[test]
337 fn test_move_cursor_left() {
338 let mut app = CalculatorApp::new();
339 app.set_input("123");
340 app.move_cursor_left();
341 assert_eq!(app.cursor(), 2);
342 }
343
344 #[test]
345 fn test_move_cursor_left_at_start() {
346 let mut app = CalculatorApp::new();
347 app.set_input("123");
348 app.set_cursor(0);
349 app.move_cursor_left(); assert_eq!(app.cursor(), 0);
351 }
352
353 #[test]
354 fn test_move_cursor_right() {
355 let mut app = CalculatorApp::new();
356 app.set_input("123");
357 app.set_cursor(0);
358 app.move_cursor_right();
359 assert_eq!(app.cursor(), 1);
360 }
361
362 #[test]
363 fn test_move_cursor_right_at_end() {
364 let mut app = CalculatorApp::new();
365 app.set_input("123");
366 app.move_cursor_right(); assert_eq!(app.cursor(), 3);
368 }
369
370 #[test]
371 fn test_move_cursor_start() {
372 let mut app = CalculatorApp::new();
373 app.set_input("123");
374 app.move_cursor_start();
375 assert_eq!(app.cursor(), 0);
376 }
377
378 #[test]
379 fn test_move_cursor_end() {
380 let mut app = CalculatorApp::new();
381 app.set_input("123");
382 app.set_cursor(0);
383 app.move_cursor_end();
384 assert_eq!(app.cursor(), 3);
385 }
386
387 #[test]
390 fn test_clear() {
391 let mut app = CalculatorApp::new();
392 app.set_input("2 + 2");
393 app.evaluate();
394 app.clear();
395 assert!(app.input().is_empty());
396 assert_eq!(app.cursor(), 0);
397 assert!(app.result().is_none());
398 assert!(!app.history().is_empty());
400 }
401
402 #[test]
403 fn test_clear_all() {
404 let mut app = CalculatorApp::new();
405 app.set_input("2 + 2");
406 app.evaluate();
407 app.clear_all();
408 assert!(app.input().is_empty());
409 assert!(app.history().is_empty());
410 }
411
412 #[test]
415 fn test_evaluate_simple() {
416 let mut app = CalculatorApp::new();
417 app.set_input("2 + 3");
418 app.evaluate();
419 assert_eq!(app.result(), Some(&Ok(5.0)));
420 }
421
422 #[test]
423 fn test_evaluate_empty() {
424 let mut app = CalculatorApp::new();
425 app.evaluate(); assert!(app.result().is_none());
427 }
428
429 #[test]
430 fn test_evaluate_error() {
431 let mut app = CalculatorApp::new();
432 app.set_input("1 / 0");
433 app.evaluate();
434 assert!(matches!(app.result(), Some(Err(CalcError::DivisionByZero))));
435 }
436
437 #[test]
438 fn test_evaluate_records_history() {
439 let mut app = CalculatorApp::new();
440 app.set_input("2 + 2");
441 app.evaluate();
442 assert_eq!(app.history().len(), 1);
443 assert_eq!(app.history().last().unwrap().result, 4.0);
444 }
445
446 #[test]
447 fn test_evaluate_error_not_in_history() {
448 let mut app = CalculatorApp::new();
449 app.set_input("1 / 0");
450 app.evaluate();
451 assert!(app.history().is_empty());
452 }
453
454 #[test]
457 fn test_result_display_none() {
458 let app = CalculatorApp::new();
459 assert_eq!(app.result_display(), "");
460 }
461
462 #[test]
463 fn test_result_display_integer() {
464 let mut app = CalculatorApp::new();
465 app.set_input("2 + 2");
466 app.evaluate();
467 assert_eq!(app.result_display(), "4");
468 }
469
470 #[test]
471 fn test_result_display_decimal() {
472 let mut app = CalculatorApp::new();
473 app.set_input("1 / 3");
474 app.evaluate();
475 let display = app.result_display();
476 assert!(display.starts_with("0.333"));
477 }
478
479 #[test]
480 fn test_result_display_error() {
481 let mut app = CalculatorApp::new();
482 app.set_input("1 / 0");
483 app.evaluate();
484 assert!(app.result_display().contains("Error"));
485 }
486
487 #[test]
490 fn test_quit() {
491 let mut app = CalculatorApp::new();
492 assert!(!app.should_quit());
493 app.quit();
494 assert!(app.should_quit());
495 }
496
497 #[test]
500 fn test_recall_last() {
501 let mut app = CalculatorApp::new();
502 app.set_input("5 * 5");
503 app.evaluate();
504 app.clear();
505 app.recall_last();
506 assert_eq!(app.input(), "5 * 5");
507 }
508
509 #[test]
510 fn test_recall_last_empty_history() {
511 let mut app = CalculatorApp::new();
512 app.recall_last(); assert!(app.input().is_empty());
514 }
515
516 #[test]
519 fn test_jidoka_status_ready() {
520 let app = CalculatorApp::new();
521 let status = app.jidoka_status();
522 assert!(status.contains(&"Ready".to_string()));
523 }
524
525 #[test]
526 fn test_jidoka_status_success() {
527 let mut app = CalculatorApp::new();
528 app.set_input("2 + 2");
529 app.evaluate();
530 let status = app.jidoka_status();
531 assert!(status.iter().any(|s| s.contains("✓")));
532 }
533
534 #[test]
535 fn test_jidoka_status_violation() {
536 let validator = AnomalyValidator::with_max_magnitude(10.0);
537 let mut app = CalculatorApp::with_validator(validator);
538 app.set_input("5 * 5");
539 app.evaluate();
540 let status = app.jidoka_status();
541 assert!(status.iter().any(|s| s.contains("✗")));
542 }
543
544 #[test]
545 fn test_jidoka_status_error() {
546 let mut app = CalculatorApp::new();
547 app.set_input("1 / 0");
548 app.evaluate();
549 let status = app.jidoka_status();
550 assert!(status.iter().any(|s| s.contains("Error")));
551 }
552
553 #[test]
556 fn test_format_result_integer() {
557 assert_eq!(format_result(42.0), "42");
558 }
559
560 #[test]
561 fn test_format_result_negative_integer() {
562 assert_eq!(format_result(-42.0), "-42");
563 }
564
565 #[test]
566 fn test_format_result_decimal() {
567 assert_eq!(format_result(3.14), "3.14");
568 }
569
570 #[test]
571 fn test_format_result_trailing_zeros() {
572 assert_eq!(format_result(1.50), "1.5");
573 }
574
575 #[test]
576 fn test_format_result_large_integer() {
577 assert_eq!(format_result(1e14), "100000000000000");
578 }
579
580 #[test]
581 fn test_format_result_very_large() {
582 let result = format_result(1e16);
584 assert!(result.contains('e') || result.len() > 15);
585 }
586}