Skip to main content

oxigdal_sync/crdt/
pn_counter.rs

1//! Positive-Negative Counter CRDT
2//!
3//! A counter that supports both increment and decrement operations.
4
5use crate::crdt::{Crdt, DeviceAware, GCounter};
6use crate::{DeviceId, SyncResult};
7use serde::{Deserialize, Serialize};
8
9/// Positive-Negative Counter
10///
11/// A CRDT counter that supports both increment and decrement operations.
12/// Implemented using two G-Counters: one for increments (positive) and
13/// one for decrements (negative). The value is the difference.
14///
15/// # Example
16///
17/// ```rust
18/// use oxigdal_sync::crdt::{PnCounter, Crdt};
19///
20/// let mut counter1 = PnCounter::new("device-1".to_string());
21/// counter1.increment(10);
22/// counter1.decrement(3);
23///
24/// let mut counter2 = PnCounter::new("device-2".to_string());
25/// counter2.increment(5);
26///
27/// counter1.merge(&counter2).ok();
28/// assert_eq!(counter1.value(), 12); // 10 - 3 + 5
29/// ```
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct PnCounter {
32    /// Positive increments
33    positive: GCounter,
34    /// Negative decrements
35    negative: GCounter,
36    /// Device ID
37    device_id: DeviceId,
38}
39
40impl PnCounter {
41    /// Creates a new PN-Counter
42    ///
43    /// # Arguments
44    ///
45    /// * `device_id` - The device ID
46    pub fn new(device_id: DeviceId) -> Self {
47        Self {
48            positive: GCounter::new(device_id.clone()),
49            negative: GCounter::new(device_id.clone()),
50            device_id,
51        }
52    }
53
54    /// Increments the counter by the given amount
55    ///
56    /// # Arguments
57    ///
58    /// * `delta` - The amount to increment by
59    ///
60    /// # Returns
61    ///
62    /// The new total value
63    pub fn increment(&mut self, delta: u64) -> i64 {
64        self.positive.increment(delta);
65        self.value()
66    }
67
68    /// Decrements the counter by the given amount
69    ///
70    /// # Arguments
71    ///
72    /// * `delta` - The amount to decrement by
73    ///
74    /// # Returns
75    ///
76    /// The new total value
77    pub fn decrement(&mut self, delta: u64) -> i64 {
78        self.negative.increment(delta);
79        self.value()
80    }
81
82    /// Increments the counter by 1
83    ///
84    /// # Returns
85    ///
86    /// The new total value
87    pub fn inc(&mut self) -> i64 {
88        self.increment(1)
89    }
90
91    /// Decrements the counter by 1
92    ///
93    /// # Returns
94    ///
95    /// The new total value
96    pub fn dec(&mut self) -> i64 {
97        self.decrement(1)
98    }
99
100    /// Gets the current value
101    ///
102    /// This is the difference between positive and negative counts.
103    pub fn value(&self) -> i64 {
104        let pos = self.positive.value() as i64;
105        let neg = self.negative.value() as i64;
106        pos - neg
107    }
108
109    /// Gets the positive counter
110    pub fn positive_counter(&self) -> &GCounter {
111        &self.positive
112    }
113
114    /// Gets the negative counter
115    pub fn negative_counter(&self) -> &GCounter {
116        &self.negative
117    }
118
119    /// Gets the total number of increments
120    pub fn total_increments(&self) -> u64 {
121        self.positive.value()
122    }
123
124    /// Gets the total number of decrements
125    pub fn total_decrements(&self) -> u64 {
126        self.negative.value()
127    }
128}
129
130impl Crdt for PnCounter {
131    fn merge(&mut self, other: &Self) -> SyncResult<()> {
132        self.positive.merge(&other.positive)?;
133        self.negative.merge(&other.negative)?;
134        Ok(())
135    }
136
137    fn dominated_by(&self, other: &Self) -> bool {
138        self.positive.dominated_by(&other.positive) && self.negative.dominated_by(&other.negative)
139    }
140}
141
142impl DeviceAware for PnCounter {
143    fn device_id(&self) -> &DeviceId {
144        &self.device_id
145    }
146
147    fn set_device_id(&mut self, device_id: DeviceId) {
148        self.device_id = device_id.clone();
149        self.positive.set_device_id(device_id.clone());
150        self.negative.set_device_id(device_id);
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_pn_counter_creation() {
160        let counter = PnCounter::new("device-1".to_string());
161        assert_eq!(counter.value(), 0);
162        assert_eq!(counter.device_id(), "device-1");
163    }
164
165    #[test]
166    fn test_pn_counter_increment() {
167        let mut counter = PnCounter::new("device-1".to_string());
168        counter.increment(5);
169        assert_eq!(counter.value(), 5);
170        counter.increment(3);
171        assert_eq!(counter.value(), 8);
172    }
173
174    #[test]
175    fn test_pn_counter_decrement() {
176        let mut counter = PnCounter::new("device-1".to_string());
177        counter.decrement(5);
178        assert_eq!(counter.value(), -5);
179        counter.decrement(3);
180        assert_eq!(counter.value(), -8);
181    }
182
183    #[test]
184    fn test_pn_counter_inc_dec() {
185        let mut counter = PnCounter::new("device-1".to_string());
186        counter.inc();
187        counter.inc();
188        counter.inc();
189        assert_eq!(counter.value(), 3);
190        counter.dec();
191        assert_eq!(counter.value(), 2);
192    }
193
194    #[test]
195    fn test_pn_counter_mixed_operations() {
196        let mut counter = PnCounter::new("device-1".to_string());
197        counter.increment(10);
198        counter.decrement(3);
199        counter.increment(5);
200        counter.decrement(2);
201        assert_eq!(counter.value(), 10); // 10 - 3 + 5 - 2
202    }
203
204    #[test]
205    fn test_pn_counter_merge() {
206        let mut counter1 = PnCounter::new("device-1".to_string());
207        let mut counter2 = PnCounter::new("device-2".to_string());
208
209        counter1.increment(10);
210        counter1.decrement(3);
211
212        counter2.increment(5);
213        counter2.decrement(2);
214
215        counter1.merge(&counter2).ok();
216
217        assert_eq!(counter1.value(), 10); // (10 + 5) - (3 + 2)
218    }
219
220    #[test]
221    fn test_pn_counter_dominated_by() {
222        let mut counter1 = PnCounter::new("device-1".to_string());
223        let mut counter2 = PnCounter::new("device-1".to_string());
224
225        counter1.increment(3);
226        counter1.decrement(1);
227
228        counter2.increment(5);
229        counter2.decrement(2);
230
231        assert!(counter1.dominated_by(&counter2));
232        assert!(!counter2.dominated_by(&counter1));
233    }
234
235    #[test]
236    fn test_pn_counter_total_ops() {
237        let mut counter = PnCounter::new("device-1".to_string());
238        counter.increment(10);
239        counter.decrement(3);
240        counter.increment(5);
241
242        assert_eq!(counter.total_increments(), 15);
243        assert_eq!(counter.total_decrements(), 3);
244        assert_eq!(counter.value(), 12);
245    }
246}