Skip to main content

aprender_test_showcase/core/
history.rs

1//! Calculation history with 100% test coverage
2//!
3//! Probar: Visual feedback (Visualization) - Track and display calculation history
4
5use serde::{Deserialize, Serialize};
6use std::collections::VecDeque;
7
8/// A single entry in the calculation history
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct HistoryEntry {
11    /// The expression that was evaluated
12    pub expression: String,
13    /// The result of the calculation
14    pub result: f64,
15    /// Timestamp of when the calculation was performed (Unix epoch millis)
16    pub timestamp: u64,
17}
18
19impl HistoryEntry {
20    /// Creates a new history entry
21    #[must_use]
22    pub fn new(expression: String, result: f64) -> Self {
23        Self {
24            expression,
25            result,
26            timestamp: Self::current_timestamp(),
27        }
28    }
29
30    /// Creates a history entry with a specific timestamp (for testing)
31    #[must_use]
32    pub fn with_timestamp(expression: String, result: f64, timestamp: u64) -> Self {
33        Self {
34            expression,
35            result,
36            timestamp,
37        }
38    }
39
40    /// Returns the current timestamp in milliseconds
41    fn current_timestamp() -> u64 {
42        use std::time::{SystemTime, UNIX_EPOCH};
43        SystemTime::now()
44            .duration_since(UNIX_EPOCH)
45            .map(|d| d.as_millis() as u64)
46            .unwrap_or(0)
47    }
48
49    /// Returns a formatted display string
50    #[must_use]
51    pub fn display(&self) -> String {
52        format!("{} = {}", self.expression, self.result)
53    }
54}
55
56/// Calculator history manager
57///
58/// Tracks past calculations for display and recall.
59/// Implements a bounded queue to prevent unbounded memory growth.
60#[derive(Debug, Clone)]
61pub struct History {
62    /// The history entries
63    entries: VecDeque<HistoryEntry>,
64    /// Maximum number of entries to keep
65    max_entries: usize,
66}
67
68impl Default for History {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl History {
75    /// Default maximum history size
76    pub const DEFAULT_MAX_ENTRIES: usize = 100;
77
78    /// Creates a new history with default capacity
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            entries: VecDeque::new(),
83            max_entries: Self::DEFAULT_MAX_ENTRIES,
84        }
85    }
86
87    /// Creates a history with custom maximum size
88    #[must_use]
89    pub fn with_capacity(max_entries: usize) -> Self {
90        Self {
91            entries: VecDeque::with_capacity(max_entries),
92            max_entries,
93        }
94    }
95
96    /// Adds an entry to the history
97    pub fn push(&mut self, entry: HistoryEntry) {
98        if self.entries.len() >= self.max_entries {
99            self.entries.pop_front();
100        }
101        self.entries.push_back(entry);
102    }
103
104    /// Adds a calculation result to the history
105    pub fn record(&mut self, expression: &str, result: f64) {
106        let entry = HistoryEntry::new(expression.to_string(), result);
107        self.push(entry);
108    }
109
110    /// Returns the number of entries
111    #[must_use]
112    pub fn len(&self) -> usize {
113        self.entries.len()
114    }
115
116    /// Returns true if the history is empty
117    #[must_use]
118    pub fn is_empty(&self) -> bool {
119        self.entries.is_empty()
120    }
121
122    /// Returns the maximum number of entries
123    #[must_use]
124    pub fn max_entries(&self) -> usize {
125        self.max_entries
126    }
127
128    /// Clears all history entries
129    pub fn clear(&mut self) {
130        self.entries.clear();
131    }
132
133    /// Returns an iterator over the entries (oldest first)
134    pub fn iter(&self) -> impl Iterator<Item = &HistoryEntry> {
135        self.entries.iter()
136    }
137
138    /// Returns an iterator over the entries (newest first)
139    pub fn iter_rev(&self) -> impl Iterator<Item = &HistoryEntry> {
140        self.entries.iter().rev()
141    }
142
143    /// Returns the most recent entry
144    #[must_use]
145    pub fn last(&self) -> Option<&HistoryEntry> {
146        self.entries.back()
147    }
148
149    /// Returns the oldest entry
150    #[must_use]
151    pub fn first(&self) -> Option<&HistoryEntry> {
152        self.entries.front()
153    }
154
155    /// Returns the entry at the given index (0 = oldest)
156    #[must_use]
157    pub fn get(&self, index: usize) -> Option<&HistoryEntry> {
158        self.entries.get(index)
159    }
160
161    /// Returns the last n entries (newest first)
162    #[must_use]
163    pub fn last_n(&self, n: usize) -> Vec<&HistoryEntry> {
164        self.entries.iter().rev().take(n).collect()
165    }
166
167    /// Serializes the history to JSON
168    pub fn to_json(&self) -> Result<String, serde_json::Error> {
169        serde_json::to_string(&self.entries.iter().collect::<Vec<_>>())
170    }
171
172    /// Deserializes history from JSON
173    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
174        let entries: Vec<HistoryEntry> = serde_json::from_str(json)?;
175        let mut history = Self::new();
176        for entry in entries {
177            history.push(entry);
178        }
179        Ok(history)
180    }
181
182    /// Exports history to a formatted string
183    #[must_use]
184    pub fn export_formatted(&self) -> String {
185        self.entries
186            .iter()
187            .map(HistoryEntry::display)
188            .collect::<Vec<_>>()
189            .join("\n")
190    }
191}
192
193#[cfg(test)]
194#[allow(clippy::unwrap_used, clippy::float_cmp)]
195mod tests {
196    use super::*;
197
198    // ===== HistoryEntry tests =====
199
200    #[test]
201    fn test_history_entry_new() {
202        let entry = HistoryEntry::new("2 + 2".into(), 4.0);
203        assert_eq!(entry.expression, "2 + 2");
204        assert_eq!(entry.result, 4.0);
205        assert!(entry.timestamp > 0);
206    }
207
208    #[test]
209    fn test_history_entry_with_timestamp() {
210        let entry = HistoryEntry::with_timestamp("3 * 3".into(), 9.0, 1234567890);
211        assert_eq!(entry.expression, "3 * 3");
212        assert_eq!(entry.result, 9.0);
213        assert_eq!(entry.timestamp, 1234567890);
214    }
215
216    #[test]
217    fn test_history_entry_display() {
218        let entry = HistoryEntry::new("5 + 3".into(), 8.0);
219        assert_eq!(entry.display(), "5 + 3 = 8");
220    }
221
222    #[test]
223    fn test_history_entry_clone() {
224        let entry = HistoryEntry::new("1 + 1".into(), 2.0);
225        let cloned = entry.clone();
226        assert_eq!(entry, cloned);
227    }
228
229    #[test]
230    fn test_history_entry_serialize() {
231        let entry = HistoryEntry::with_timestamp("2 ^ 3".into(), 8.0, 1000);
232        let json = serde_json::to_string(&entry).unwrap();
233        assert!(json.contains("\"expression\":\"2 ^ 3\""));
234        assert!(json.contains("\"result\":8.0"));
235    }
236
237    #[test]
238    fn test_history_entry_deserialize() {
239        let json = r#"{"expression":"10 / 2","result":5.0,"timestamp":2000}"#;
240        let entry: HistoryEntry = serde_json::from_str(json).unwrap();
241        assert_eq!(entry.expression, "10 / 2");
242        assert_eq!(entry.result, 5.0);
243        assert_eq!(entry.timestamp, 2000);
244    }
245
246    // ===== History tests =====
247
248    #[test]
249    fn test_history_new() {
250        let history = History::new();
251        assert!(history.is_empty());
252        assert_eq!(history.len(), 0);
253        assert_eq!(history.max_entries(), History::DEFAULT_MAX_ENTRIES);
254    }
255
256    #[test]
257    fn test_history_default() {
258        let history = History::default();
259        assert!(history.is_empty());
260    }
261
262    #[test]
263    fn test_history_with_capacity() {
264        let history = History::with_capacity(50);
265        assert_eq!(history.max_entries(), 50);
266    }
267
268    #[test]
269    fn test_history_push() {
270        let mut history = History::new();
271        let entry = HistoryEntry::new("1 + 1".into(), 2.0);
272        history.push(entry);
273        assert_eq!(history.len(), 1);
274    }
275
276    #[test]
277    fn test_history_record() {
278        let mut history = History::new();
279        history.record("3 + 4", 7.0);
280        assert_eq!(history.len(), 1);
281        assert_eq!(history.last().unwrap().expression, "3 + 4");
282        assert_eq!(history.last().unwrap().result, 7.0);
283    }
284
285    #[test]
286    fn test_history_max_entries_enforcement() {
287        let mut history = History::with_capacity(3);
288        history.record("1", 1.0);
289        history.record("2", 2.0);
290        history.record("3", 3.0);
291        history.record("4", 4.0);
292
293        assert_eq!(history.len(), 3);
294        // First entry (1) should be removed
295        assert_eq!(history.first().unwrap().result, 2.0);
296        assert_eq!(history.last().unwrap().result, 4.0);
297    }
298
299    #[test]
300    fn test_history_clear() {
301        let mut history = History::new();
302        history.record("1", 1.0);
303        history.record("2", 2.0);
304        history.clear();
305        assert!(history.is_empty());
306    }
307
308    #[test]
309    fn test_history_iter() {
310        let mut history = History::new();
311        history.record("a", 1.0);
312        history.record("b", 2.0);
313        history.record("c", 3.0);
314
315        let results: Vec<f64> = history.iter().map(|e| e.result).collect();
316        assert_eq!(results, vec![1.0, 2.0, 3.0]);
317    }
318
319    #[test]
320    fn test_history_iter_rev() {
321        let mut history = History::new();
322        history.record("a", 1.0);
323        history.record("b", 2.0);
324        history.record("c", 3.0);
325
326        let results: Vec<f64> = history.iter_rev().map(|e| e.result).collect();
327        assert_eq!(results, vec![3.0, 2.0, 1.0]);
328    }
329
330    #[test]
331    fn test_history_last() {
332        let mut history = History::new();
333        assert!(history.last().is_none());
334
335        history.record("x", 10.0);
336        assert_eq!(history.last().unwrap().result, 10.0);
337
338        history.record("y", 20.0);
339        assert_eq!(history.last().unwrap().result, 20.0);
340    }
341
342    #[test]
343    fn test_history_first() {
344        let mut history = History::new();
345        assert!(history.first().is_none());
346
347        history.record("x", 10.0);
348        assert_eq!(history.first().unwrap().result, 10.0);
349
350        history.record("y", 20.0);
351        assert_eq!(history.first().unwrap().result, 10.0);
352    }
353
354    #[test]
355    fn test_history_get() {
356        let mut history = History::new();
357        history.record("a", 1.0);
358        history.record("b", 2.0);
359        history.record("c", 3.0);
360
361        assert_eq!(history.get(0).unwrap().result, 1.0);
362        assert_eq!(history.get(1).unwrap().result, 2.0);
363        assert_eq!(history.get(2).unwrap().result, 3.0);
364        assert!(history.get(3).is_none());
365    }
366
367    #[test]
368    fn test_history_last_n() {
369        let mut history = History::new();
370        history.record("a", 1.0);
371        history.record("b", 2.0);
372        history.record("c", 3.0);
373        history.record("d", 4.0);
374
375        let last_2: Vec<f64> = history.last_n(2).iter().map(|e| e.result).collect();
376        assert_eq!(last_2, vec![4.0, 3.0]);
377
378        let last_10 = history.last_n(10);
379        assert_eq!(last_10.len(), 4);
380    }
381
382    #[test]
383    fn test_history_to_json() {
384        let mut history = History::new();
385        history.push(HistoryEntry::with_timestamp("1+1".into(), 2.0, 1000));
386        history.push(HistoryEntry::with_timestamp("2+2".into(), 4.0, 2000));
387
388        let json = history.to_json().unwrap();
389        assert!(json.contains("1+1"));
390        assert!(json.contains("2+2"));
391    }
392
393    #[test]
394    fn test_history_from_json() {
395        let json = r#"[
396            {"expression":"a","result":1.0,"timestamp":1000},
397            {"expression":"b","result":2.0,"timestamp":2000}
398        ]"#;
399
400        let history = History::from_json(json).unwrap();
401        assert_eq!(history.len(), 2);
402        assert_eq!(history.first().unwrap().expression, "a");
403        assert_eq!(history.last().unwrap().expression, "b");
404    }
405
406    #[test]
407    fn test_history_from_json_invalid() {
408        let result = History::from_json("invalid json");
409        assert!(result.is_err());
410    }
411
412    #[test]
413    fn test_history_export_formatted() {
414        let mut history = History::new();
415        history.push(HistoryEntry::with_timestamp("1+1".into(), 2.0, 1000));
416        history.push(HistoryEntry::with_timestamp("2*3".into(), 6.0, 2000));
417
418        let formatted = history.export_formatted();
419        assert_eq!(formatted, "1+1 = 2\n2*3 = 6");
420    }
421
422    #[test]
423    fn test_history_export_formatted_empty() {
424        let history = History::new();
425        let formatted = history.export_formatted();
426        assert_eq!(formatted, "");
427    }
428
429    #[test]
430    fn test_history_clone() {
431        let mut history = History::new();
432        history.record("test", 42.0);
433
434        let cloned = history.clone();
435        assert_eq!(cloned.len(), 1);
436        assert_eq!(cloned.last().unwrap().result, 42.0);
437    }
438
439    #[test]
440    fn test_history_round_trip_json() {
441        let mut original = History::new();
442        original.push(HistoryEntry::with_timestamp("x".into(), 10.0, 100));
443        original.push(HistoryEntry::with_timestamp("y".into(), 20.0, 200));
444
445        let json = original.to_json().unwrap();
446        let restored = History::from_json(&json).unwrap();
447
448        assert_eq!(original.len(), restored.len());
449        for (orig, rest) in original.iter().zip(restored.iter()) {
450            assert_eq!(orig, rest);
451        }
452    }
453}