Skip to main content

showcase_calculator/wasm/
calculator.rs

1//! WASM Calculator Bindings
2//!
3//! This module provides the WASM-specific calculator implementation
4//! that wraps the core calculator for browser usage.
5//!
6//! Probar: Error prevention - Type-safe bindings prevent invalid states
7
8use crate::core::evaluator::Evaluator;
9use crate::core::history::{History, HistoryEntry};
10use crate::core::{AnomalyValidator, CalcError, CalcResult};
11
12/// WASM Calculator - browser-ready calculator
13#[derive(Debug)]
14pub struct WasmCalculator {
15    /// Expression evaluator
16    evaluator: Evaluator,
17    /// Expression history
18    history: History,
19    /// Current input expression
20    input: String,
21    /// Last result (if any)
22    last_result: Option<Result<f64, CalcError>>,
23    /// Anomaly status messages
24    jidoka_status: Vec<String>,
25}
26
27impl Default for WasmCalculator {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl WasmCalculator {
34    /// Creates a new WASM calculator
35    #[must_use]
36    pub fn new() -> Self {
37        Self {
38            evaluator: Evaluator::new(),
39            history: History::new(),
40            input: String::new(),
41            last_result: None,
42            jidoka_status: vec!["Ready".to_string()],
43        }
44    }
45
46    /// Creates a calculator with custom Anomaly validator
47    #[must_use]
48    pub fn with_validator(validator: AnomalyValidator) -> Self {
49        Self {
50            evaluator: Evaluator::with_validator(validator),
51            history: History::new(),
52            input: String::new(),
53            last_result: None,
54            jidoka_status: vec!["Ready (with validator)".to_string()],
55        }
56    }
57
58    /// Sets the input expression
59    pub fn set_input(&mut self, expr: &str) {
60        self.input = expr.to_string();
61    }
62
63    /// Gets the current input
64    #[must_use]
65    pub fn input(&self) -> &str {
66        &self.input
67    }
68
69    /// Appends to the input
70    pub fn append_input(&mut self, s: &str) {
71        self.input.push_str(s);
72    }
73
74    /// Removes the last character from input
75    pub fn backspace(&mut self) {
76        self.input.pop();
77    }
78
79    /// Clears all state
80    pub fn clear(&mut self) {
81        self.input.clear();
82        self.last_result = None;
83        self.jidoka_status = vec!["Cleared".to_string()];
84    }
85
86    /// Clears only the input (keeps result)
87    pub fn clear_input(&mut self) {
88        self.input.clear();
89    }
90
91    /// Evaluates the current input expression
92    pub fn evaluate(&mut self) -> CalcResult<f64> {
93        if self.input.is_empty() {
94            let err = CalcError::EmptyExpression;
95            self.last_result = Some(Err(err.clone()));
96            self.jidoka_status = vec!["Error: Empty expression".to_string()];
97            return Err(err);
98        }
99
100        let result = self.evaluator.evaluate_str(&self.input);
101
102        match &result {
103            Ok(value) => {
104                self.history.record(&self.input, *value);
105                self.jidoka_status = vec![
106                    format!("✓ Result: {}", value),
107                    "✓ Calculation successful".to_string(),
108                    "✓ All constraints satisfied".to_string(),
109                ];
110            }
111            Err(e) => {
112                self.jidoka_status = vec![format!("✗ Error: {}", e)];
113            }
114        }
115
116        self.last_result = Some(result.clone());
117        result
118    }
119
120    /// Gets the last result
121    #[must_use]
122    pub fn result(&self) -> Option<&Result<f64, CalcError>> {
123        self.last_result.as_ref()
124    }
125
126    /// Gets a formatted result display string
127    #[must_use]
128    pub fn result_display(&self) -> String {
129        match &self.last_result {
130            Some(Ok(value)) => format_number(*value),
131            Some(Err(e)) => format!("Error: {}", e),
132            None => String::new(),
133        }
134    }
135
136    /// Gets the calculation history
137    #[must_use]
138    pub fn history(&self) -> &History {
139        &self.history
140    }
141
142    /// Gets history as JSON-like string array (for WASM interop)
143    #[must_use]
144    pub fn history_entries(&self) -> Vec<String> {
145        self.history
146            .iter()
147            .map(|entry| format!("{} = {}", entry.expression, format_number(entry.result)))
148            .collect()
149    }
150
151    /// Gets history entries in reverse order (newest first)
152    #[must_use]
153    pub fn history_entries_rev(&self) -> Vec<String> {
154        self.history
155            .iter_rev()
156            .map(|entry| format!("{} = {}", entry.expression, format_number(entry.result)))
157            .collect()
158    }
159
160    /// Gets the Anomaly status messages
161    #[must_use]
162    pub fn jidoka_status(&self) -> &[String] {
163        &self.jidoka_status
164    }
165
166    /// Clears the history
167    pub fn clear_history(&mut self) {
168        self.history.clear();
169        self.evaluator.clear_history();
170    }
171
172    /// Gets the number of history entries
173    #[must_use]
174    pub fn history_len(&self) -> usize {
175        self.history.len()
176    }
177
178    /// Recalls a history entry by index
179    pub fn recall_history(&self, index: usize) -> Option<&HistoryEntry> {
180        self.history.get(index)
181    }
182
183    /// Uses the last result as input for chained calculations
184    pub fn use_last_result(&mut self) {
185        if let Some(Ok(value)) = &self.last_result {
186            self.input = format_number(*value);
187        }
188    }
189}
190
191/// Formats a number for display (removes trailing zeros)
192fn format_number(n: f64) -> String {
193    if n.fract() == 0.0 {
194        format!("{}", n as i64)
195    } else {
196        // Format with reasonable precision, trim trailing zeros
197        let s = format!("{:.10}", n);
198        let s = s.trim_end_matches('0');
199        let s = s.trim_end_matches('.');
200        s.to_string()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    // ===== Constructor tests =====
209
210    #[test]
211    fn test_wasm_calculator_new() {
212        let calc = WasmCalculator::new();
213        assert!(calc.input().is_empty());
214        assert!(calc.result().is_none());
215    }
216
217    #[test]
218    fn test_wasm_calculator_default() {
219        let calc = WasmCalculator::default();
220        assert!(calc.input().is_empty());
221    }
222
223    #[test]
224    fn test_wasm_calculator_with_validator() {
225        let validator = AnomalyValidator::with_max_magnitude(100.0);
226        let mut calc = WasmCalculator::with_validator(validator);
227        // Should fail because 200 > 100
228        calc.set_input("100 * 2");
229        let _ = calc.evaluate();
230        assert!(matches!(
231            calc.result(),
232            Some(Err(CalcError::AnomalyViolation(_)))
233        ));
234    }
235
236    #[test]
237    fn test_wasm_calculator_debug() {
238        let calc = WasmCalculator::new();
239        let debug = format!("{:?}", calc);
240        assert!(debug.contains("WasmCalculator"));
241    }
242
243    // ===== Input tests =====
244
245    #[test]
246    fn test_set_input() {
247        let mut calc = WasmCalculator::new();
248        calc.set_input("2 + 2");
249        assert_eq!(calc.input(), "2 + 2");
250    }
251
252    #[test]
253    fn test_append_input() {
254        let mut calc = WasmCalculator::new();
255        calc.append_input("2");
256        calc.append_input("+");
257        calc.append_input("3");
258        assert_eq!(calc.input(), "2+3");
259    }
260
261    #[test]
262    fn test_backspace() {
263        let mut calc = WasmCalculator::new();
264        calc.set_input("123");
265        calc.backspace();
266        assert_eq!(calc.input(), "12");
267    }
268
269    #[test]
270    fn test_backspace_empty() {
271        let mut calc = WasmCalculator::new();
272        calc.backspace(); // should not panic
273        assert!(calc.input().is_empty());
274    }
275
276    #[test]
277    fn test_clear() {
278        let mut calc = WasmCalculator::new();
279        calc.set_input("5 * 5");
280        calc.evaluate().unwrap();
281        calc.clear();
282        assert!(calc.input().is_empty());
283        assert!(calc.result().is_none());
284    }
285
286    #[test]
287    fn test_clear_input() {
288        let mut calc = WasmCalculator::new();
289        calc.set_input("5 * 5");
290        calc.evaluate().unwrap();
291        calc.clear_input();
292        assert!(calc.input().is_empty());
293        assert!(calc.result().is_some()); // result preserved
294    }
295
296    // ===== Evaluation tests =====
297
298    #[test]
299    fn test_evaluate_simple() {
300        let mut calc = WasmCalculator::new();
301        calc.set_input("2 + 3");
302        let result = calc.evaluate();
303        assert_eq!(result, Ok(5.0));
304    }
305
306    #[test]
307    fn test_evaluate_complex() {
308        let mut calc = WasmCalculator::new();
309        calc.set_input("42 * (3 + 7)");
310        let result = calc.evaluate();
311        assert_eq!(result, Ok(420.0));
312    }
313
314    #[test]
315    fn test_evaluate_empty() {
316        let mut calc = WasmCalculator::new();
317        let result = calc.evaluate();
318        assert!(matches!(result, Err(CalcError::EmptyExpression)));
319    }
320
321    #[test]
322    fn test_evaluate_division_by_zero() {
323        let mut calc = WasmCalculator::new();
324        calc.set_input("1 / 0");
325        let result = calc.evaluate();
326        assert!(matches!(result, Err(CalcError::DivisionByZero)));
327    }
328
329    #[test]
330    fn test_evaluate_parse_error() {
331        let mut calc = WasmCalculator::new();
332        calc.set_input("2 + +");
333        let result = calc.evaluate();
334        assert!(matches!(result, Err(CalcError::ParseError(_))));
335    }
336
337    // ===== Result display tests =====
338
339    #[test]
340    fn test_result_display_integer() {
341        let mut calc = WasmCalculator::new();
342        calc.set_input("5 * 5");
343        calc.evaluate().unwrap();
344        assert_eq!(calc.result_display(), "25");
345    }
346
347    #[test]
348    fn test_result_display_decimal() {
349        let mut calc = WasmCalculator::new();
350        calc.set_input("7 / 2");
351        calc.evaluate().unwrap();
352        assert_eq!(calc.result_display(), "3.5");
353    }
354
355    #[test]
356    fn test_result_display_error() {
357        let mut calc = WasmCalculator::new();
358        calc.set_input("1 / 0");
359        calc.evaluate().ok();
360        assert!(calc.result_display().contains("Error"));
361    }
362
363    #[test]
364    fn test_result_display_none() {
365        let calc = WasmCalculator::new();
366        assert!(calc.result_display().is_empty());
367    }
368
369    // ===== History tests =====
370
371    #[test]
372    fn test_history_recording() {
373        let mut calc = WasmCalculator::new();
374        calc.set_input("1 + 1");
375        calc.evaluate().unwrap();
376        calc.set_input("2 + 2");
377        calc.evaluate().unwrap();
378        assert_eq!(calc.history_len(), 2);
379    }
380
381    #[test]
382    fn test_history_entries() {
383        let mut calc = WasmCalculator::new();
384        calc.set_input("3 + 3");
385        calc.evaluate().unwrap();
386        let entries = calc.history_entries();
387        assert_eq!(entries.len(), 1);
388        assert_eq!(entries[0], "3 + 3 = 6");
389    }
390
391    #[test]
392    fn test_history_entries_rev() {
393        let mut calc = WasmCalculator::new();
394        calc.set_input("1 + 1");
395        calc.evaluate().unwrap();
396        calc.set_input("2 + 2");
397        calc.evaluate().unwrap();
398        let entries = calc.history_entries_rev();
399        assert_eq!(entries[0], "2 + 2 = 4"); // newest first
400        assert_eq!(entries[1], "1 + 1 = 2");
401    }
402
403    #[test]
404    fn test_clear_history() {
405        let mut calc = WasmCalculator::new();
406        calc.set_input("5 + 5");
407        calc.evaluate().unwrap();
408        calc.clear_history();
409        assert_eq!(calc.history_len(), 0);
410    }
411
412    #[test]
413    fn test_recall_history() {
414        let mut calc = WasmCalculator::new();
415        calc.set_input("10 + 10");
416        calc.evaluate().unwrap();
417        let entry = calc.recall_history(0);
418        assert!(entry.is_some());
419        assert_eq!(entry.unwrap().expression, "10 + 10");
420    }
421
422    #[test]
423    fn test_recall_history_out_of_bounds() {
424        let calc = WasmCalculator::new();
425        assert!(calc.recall_history(0).is_none());
426    }
427
428    // ===== Anomaly status tests =====
429
430    #[test]
431    fn test_jidoka_status_initial() {
432        let calc = WasmCalculator::new();
433        assert!(!calc.jidoka_status().is_empty());
434        assert!(calc.jidoka_status()[0].contains("Ready"));
435    }
436
437    #[test]
438    fn test_jidoka_status_after_success() {
439        let mut calc = WasmCalculator::new();
440        calc.set_input("2 + 2");
441        calc.evaluate().unwrap();
442        let status = calc.jidoka_status();
443        assert!(status.iter().any(|s| s.contains('✓')));
444    }
445
446    #[test]
447    fn test_jidoka_status_after_error() {
448        let mut calc = WasmCalculator::new();
449        calc.set_input("1 / 0");
450        calc.evaluate().ok();
451        let status = calc.jidoka_status();
452        assert!(status
453            .iter()
454            .any(|s| s.contains('✗') || s.contains("Error")));
455    }
456
457    #[test]
458    fn test_jidoka_status_after_clear() {
459        let mut calc = WasmCalculator::new();
460        calc.set_input("5 + 5");
461        calc.evaluate().unwrap();
462        calc.clear();
463        assert!(calc.jidoka_status()[0].contains("Cleared"));
464    }
465
466    // ===== Chained calculation tests =====
467
468    #[test]
469    fn test_use_last_result() {
470        let mut calc = WasmCalculator::new();
471        calc.set_input("5 + 5");
472        calc.evaluate().unwrap();
473        calc.use_last_result();
474        assert_eq!(calc.input(), "10");
475    }
476
477    #[test]
478    fn test_use_last_result_no_result() {
479        let mut calc = WasmCalculator::new();
480        calc.use_last_result(); // should not panic
481        assert!(calc.input().is_empty());
482    }
483
484    #[test]
485    fn test_chained_calculations() {
486        let mut calc = WasmCalculator::new();
487        calc.set_input("10 + 10");
488        calc.evaluate().unwrap();
489        calc.use_last_result();
490        calc.append_input(" * 2");
491        calc.evaluate().unwrap();
492        assert_eq!(calc.result_display(), "40");
493    }
494
495    // ===== format_number tests =====
496
497    #[test]
498    fn test_format_number_integer() {
499        assert_eq!(format_number(42.0), "42");
500    }
501
502    #[test]
503    fn test_format_number_decimal() {
504        assert_eq!(format_number(3.5), "3.5");
505    }
506
507    #[test]
508    fn test_format_number_negative() {
509        assert_eq!(format_number(-5.0), "-5");
510    }
511
512    #[test]
513    fn test_format_number_small_decimal() {
514        assert_eq!(format_number(0.125), "0.125");
515    }
516
517    #[test]
518    fn test_format_number_trailing_zeros() {
519        assert_eq!(format_number(2.500), "2.5");
520    }
521
522    // ===== All operations test =====
523
524    #[test]
525    fn test_all_operations() {
526        let mut calc = WasmCalculator::new();
527
528        calc.set_input("10 + 5");
529        assert_eq!(calc.evaluate(), Ok(15.0));
530
531        calc.set_input("10 - 3");
532        assert_eq!(calc.evaluate(), Ok(7.0));
533
534        calc.set_input("6 * 7");
535        assert_eq!(calc.evaluate(), Ok(42.0));
536
537        calc.set_input("20 / 4");
538        assert_eq!(calc.evaluate(), Ok(5.0));
539
540        calc.set_input("17 % 5");
541        assert_eq!(calc.evaluate(), Ok(2.0));
542
543        calc.set_input("2 ^ 10");
544        assert_eq!(calc.evaluate(), Ok(1024.0));
545    }
546
547    // ===== Integration tests =====
548
549    #[test]
550    fn test_full_workflow() {
551        let mut calc = WasmCalculator::new();
552
553        // Initial state
554        assert!(calc.input().is_empty());
555        assert!(calc.result().is_none());
556
557        // Enter expression
558        calc.set_input("100 / 4");
559        assert_eq!(calc.input(), "100 / 4");
560
561        // Evaluate
562        let result = calc.evaluate();
563        assert_eq!(result, Ok(25.0));
564        assert_eq!(calc.result_display(), "25");
565
566        // Check history
567        assert_eq!(calc.history_len(), 1);
568        let entries = calc.history_entries();
569        assert_eq!(entries[0], "100 / 4 = 25");
570
571        // Chain calculation
572        calc.use_last_result();
573        calc.append_input(" + 75");
574        calc.evaluate().unwrap();
575        assert_eq!(calc.result_display(), "100");
576
577        // Clear
578        calc.clear();
579        assert!(calc.input().is_empty());
580        assert!(calc.result().is_none());
581    }
582}