use std::collections::BTreeMap;
use crate::datatypes::{ActorId, Crdt};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PnCounter {
pos: BTreeMap<ActorId, u64>,
neg: BTreeMap<ActorId, u64>,
}
impl PnCounter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn apply(&mut self, actor: &ActorId, delta: i64) {
if delta >= 0 {
let entry = self.pos.entry(actor.clone()).or_insert(0);
*entry = entry.saturating_add(delta.unsigned_abs());
} else {
let entry = self.neg.entry(actor.clone()).or_insert(0);
*entry = entry.saturating_add(delta.unsigned_abs());
}
}
pub fn increment(&mut self, actor: &ActorId, by: u64) {
let entry = self.pos.entry(actor.clone()).or_insert(0);
*entry = entry.saturating_add(by);
}
pub fn decrement(&mut self, actor: &ActorId, by: u64) {
let entry = self.neg.entry(actor.clone()).or_insert(0);
*entry = entry.saturating_add(by);
}
}
impl Crdt for PnCounter {
type Value = i64;
fn merge(&mut self, other: &Self) {
for (actor, &count) in &other.pos {
let entry = self.pos.entry(actor.clone()).or_insert(0);
if *entry < count {
*entry = count;
}
}
for (actor, &count) in &other.neg {
let entry = self.neg.entry(actor.clone()).or_insert(0);
if *entry < count {
*entry = count;
}
}
}
fn value(&self) -> i64 {
let pos: u64 = self
.pos
.values()
.fold(0u64, |acc, &x| acc.saturating_add(x));
let neg: u64 = self
.neg
.values()
.fold(0u64, |acc, &x| acc.saturating_add(x));
let pos = i64::try_from(pos).unwrap_or(i64::MAX);
let neg = i64::try_from(neg).unwrap_or(i64::MAX);
pos.saturating_sub(neg)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn aid(name: &str) -> ActorId {
ActorId::new("dc1", name)
}
#[test]
fn fresh_counter_is_zero() {
let c = PnCounter::new();
assert_eq!(c.value(), 0);
}
#[test]
fn increment_then_decrement_yields_net() {
let a = aid("a");
let mut c = PnCounter::new();
c.increment(&a, 5);
c.decrement(&a, 2);
assert_eq!(c.value(), 3);
}
#[test]
fn apply_routes_signed_delta() {
let a = aid("a");
let mut c = PnCounter::new();
c.apply(&a, 10);
c.apply(&a, -3);
assert_eq!(c.value(), 7);
}
#[test]
fn merge_two_actors_sums_their_contributions() {
let a = aid("a");
let b = aid("b");
let mut x = PnCounter::new();
x.increment(&a, 5);
let mut y = PnCounter::new();
y.increment(&b, 7);
x.merge(&y);
assert_eq!(x.value(), 12);
}
#[test]
fn merge_takes_pointwise_max_per_actor() {
let a = aid("a");
let mut x = PnCounter::new();
x.increment(&a, 3);
let mut y = PnCounter::new();
y.increment(&a, 5);
x.merge(&y);
assert_eq!(x.value(), 5);
}
#[test]
fn merge_is_idempotent() {
let a = aid("a");
let mut x = PnCounter::new();
x.increment(&a, 4);
let snapshot = x.clone();
x.merge(&snapshot);
assert_eq!(x.value(), 4);
assert_eq!(x, snapshot);
}
#[test]
fn merge_is_commutative() {
let a = aid("a");
let b = aid("b");
let mut x = PnCounter::new();
x.increment(&a, 3);
x.decrement(&b, 1);
let mut y = PnCounter::new();
y.increment(&b, 4);
y.decrement(&a, 2);
let mut left = x.clone();
left.merge(&y);
let mut right = y.clone();
right.merge(&x);
assert_eq!(left, right);
}
#[test]
fn concurrent_increments_then_merge_sums_distinct_actors() {
let a = aid("a");
let b = aid("b");
let mut x = PnCounter::new();
x.increment(&a, 1);
let mut y = PnCounter::new();
y.increment(&b, 1);
x.merge(&y);
assert_eq!(x.value(), 2);
}
}