Skip to main content

oxihuman_core/
diff_tracker.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// Tracks changes (diffs) to named properties over time.
6#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub struct DiffEntry {
9    pub property: String,
10    pub old_value: String,
11    pub new_value: String,
12    pub timestamp: u64,
13}
14
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct DiffTracker {
18    entries: Vec<DiffEntry>,
19    clock: u64,
20    max_entries: usize,
21}
22
23#[allow(dead_code)]
24impl DiffTracker {
25    pub fn new(max_entries: usize) -> Self {
26        Self {
27            entries: Vec::new(),
28            clock: 0,
29            max_entries: max_entries.max(1),
30        }
31    }
32
33    pub fn advance_clock(&mut self, dt: u64) {
34        self.clock += dt;
35    }
36
37    pub fn record(&mut self, property: &str, old_value: &str, new_value: &str) {
38        if self.entries.len() >= self.max_entries {
39            self.entries.remove(0);
40        }
41        self.entries.push(DiffEntry {
42            property: property.to_string(),
43            old_value: old_value.to_string(),
44            new_value: new_value.to_string(),
45            timestamp: self.clock,
46        });
47    }
48
49    pub fn count(&self) -> usize {
50        self.entries.len()
51    }
52
53    pub fn is_empty(&self) -> bool {
54        self.entries.is_empty()
55    }
56
57    pub fn latest(&self) -> Option<&DiffEntry> {
58        self.entries.last()
59    }
60
61    pub fn by_property(&self, property: &str) -> Vec<&DiffEntry> {
62        self.entries
63            .iter()
64            .filter(|e| e.property == property)
65            .collect()
66    }
67
68    pub fn since(&self, timestamp: u64) -> Vec<&DiffEntry> {
69        self.entries
70            .iter()
71            .filter(|e| e.timestamp >= timestamp)
72            .collect()
73    }
74
75    pub fn changed_properties(&self) -> Vec<String> {
76        let mut props: Vec<String> = self.entries.iter().map(|e| e.property.clone()).collect();
77        props.sort();
78        props.dedup();
79        props
80    }
81
82    pub fn has_changes_for(&self, property: &str) -> bool {
83        self.entries.iter().any(|e| e.property == property)
84    }
85
86    pub fn clear(&mut self) {
87        self.entries.clear();
88    }
89
90    pub fn revert_latest(&mut self) -> Option<DiffEntry> {
91        self.entries.pop()
92    }
93
94    pub fn all_entries(&self) -> &[DiffEntry] {
95        &self.entries
96    }
97
98    pub fn max_entries(&self) -> usize {
99        self.max_entries
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_new() {
109        let dt = DiffTracker::new(100);
110        assert!(dt.is_empty());
111    }
112
113    #[test]
114    fn test_record() {
115        let mut dt = DiffTracker::new(100);
116        dt.record("color", "red", "blue");
117        assert_eq!(dt.count(), 1);
118    }
119
120    #[test]
121    fn test_latest() {
122        let mut dt = DiffTracker::new(100);
123        dt.record("a", "1", "2");
124        dt.record("b", "3", "4");
125        assert_eq!(dt.latest().expect("should succeed").property, "b");
126    }
127
128    #[test]
129    fn test_by_property() {
130        let mut dt = DiffTracker::new(100);
131        dt.record("x", "1", "2");
132        dt.record("y", "3", "4");
133        dt.record("x", "2", "3");
134        assert_eq!(dt.by_property("x").len(), 2);
135    }
136
137    #[test]
138    fn test_since() {
139        let mut dt = DiffTracker::new(100);
140        dt.record("a", "1", "2");
141        dt.advance_clock(10);
142        dt.record("b", "3", "4");
143        assert_eq!(dt.since(5).len(), 1);
144    }
145
146    #[test]
147    fn test_changed_properties() {
148        let mut dt = DiffTracker::new(100);
149        dt.record("b", "1", "2");
150        dt.record("a", "3", "4");
151        dt.record("b", "2", "3");
152        let props = dt.changed_properties();
153        assert_eq!(props, vec!["a", "b"]);
154    }
155
156    #[test]
157    fn test_max_entries_eviction() {
158        let mut dt = DiffTracker::new(2);
159        dt.record("a", "1", "2");
160        dt.record("b", "3", "4");
161        dt.record("c", "5", "6");
162        assert_eq!(dt.count(), 2);
163        assert!(!dt.has_changes_for("a"));
164    }
165
166    #[test]
167    fn test_revert_latest() {
168        let mut dt = DiffTracker::new(100);
169        dt.record("x", "old", "new");
170        let reverted = dt.revert_latest().expect("should succeed");
171        assert_eq!(reverted.property, "x");
172        assert!(dt.is_empty());
173    }
174
175    #[test]
176    fn test_clear() {
177        let mut dt = DiffTracker::new(100);
178        dt.record("a", "1", "2");
179        dt.clear();
180        assert!(dt.is_empty());
181    }
182
183    #[test]
184    fn test_has_changes_for() {
185        let mut dt = DiffTracker::new(100);
186        assert!(!dt.has_changes_for("x"));
187        dt.record("x", "1", "2");
188        assert!(dt.has_changes_for("x"));
189    }
190}