Skip to main content

crdt_kit/
pncounter.rs

1use crate::{Crdt, GCounter};
2
3/// A positive-negative counter (PN-Counter).
4///
5/// Supports both increment and decrement operations by maintaining two
6/// internal G-Counters: one for increments and one for decrements.
7/// The value is `increments - decrements`.
8///
9/// # Example
10///
11/// ```
12/// use crdt_kit::prelude::*;
13///
14/// let mut c1 = PNCounter::new("node-1");
15/// c1.increment();
16/// c1.increment();
17/// c1.decrement();
18/// assert_eq!(c1.value(), 1);
19///
20/// let mut c2 = PNCounter::new("node-2");
21/// c2.decrement();
22///
23/// c1.merge(&c2);
24/// assert_eq!(c1.value(), 0);
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct PNCounter {
28    increments: GCounter,
29    decrements: GCounter,
30}
31
32impl PNCounter {
33    /// Create a new PN-Counter for the given actor/replica ID.
34    pub fn new(actor: impl Into<String>) -> Self {
35        let actor = actor.into();
36        Self {
37            increments: GCounter::new(actor.clone()),
38            decrements: GCounter::new(actor),
39        }
40    }
41
42    /// Increment the counter by 1.
43    pub fn increment(&mut self) {
44        self.increments.increment();
45    }
46
47    /// Decrement the counter by 1.
48    pub fn decrement(&mut self) {
49        self.decrements.increment();
50    }
51
52    /// Get the current counter value (increments - decrements).
53    #[must_use]
54    pub fn value(&self) -> i64 {
55        self.increments.value() as i64 - self.decrements.value() as i64
56    }
57}
58
59impl Crdt for PNCounter {
60    fn merge(&mut self, other: &Self) {
61        self.increments.merge(&other.increments);
62        self.decrements.merge(&other.decrements);
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn new_counter_is_zero() {
72        let c = PNCounter::new("a");
73        assert_eq!(c.value(), 0);
74    }
75
76    #[test]
77    fn increment_and_decrement() {
78        let mut c = PNCounter::new("a");
79        c.increment();
80        c.increment();
81        c.decrement();
82        assert_eq!(c.value(), 1);
83    }
84
85    #[test]
86    fn can_go_negative() {
87        let mut c = PNCounter::new("a");
88        c.decrement();
89        c.decrement();
90        assert_eq!(c.value(), -2);
91    }
92
93    #[test]
94    fn merge_different_actors() {
95        let mut c1 = PNCounter::new("a");
96        c1.increment();
97        c1.increment();
98
99        let mut c2 = PNCounter::new("b");
100        c2.decrement();
101
102        c1.merge(&c2);
103        assert_eq!(c1.value(), 1); // 2 - 1
104    }
105
106    #[test]
107    fn merge_is_commutative() {
108        let mut c1 = PNCounter::new("a");
109        c1.increment();
110
111        let mut c2 = PNCounter::new("b");
112        c2.decrement();
113        c2.decrement();
114
115        let mut left = c1.clone();
116        left.merge(&c2);
117
118        let mut right = c2.clone();
119        right.merge(&c1);
120
121        assert_eq!(left.value(), right.value());
122    }
123
124    #[test]
125    fn merge_is_idempotent() {
126        let mut c1 = PNCounter::new("a");
127        c1.increment();
128
129        let mut c2 = PNCounter::new("b");
130        c2.decrement();
131
132        c1.merge(&c2);
133        let after_first = c1.clone();
134        c1.merge(&c2);
135
136        assert_eq!(c1, after_first);
137    }
138}