From 3001620c1eef3246f666c459f096ba390afce06b Mon Sep 17 00:00:00 2001 From: Niklas Hoefflin <122729995+itakurah@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:22:02 +0100 Subject: [PATCH] Add PN-Counter (#4974) --- DIRECTORY.md | 2 + .../datastructures/crdt/PNCounter.java | 100 ++++++++++++++++++ .../datastructures/crdt/PNCounterTest.java | 54 ++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java create mode 100644 src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java diff --git a/DIRECTORY.md b/DIRECTORY.md index 89f08c272..0548d3455 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -84,6 +84,7 @@ * [MRUCache](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/caches/MRUCache.java) * crdt * [GCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/GCounter.java) + * [PNCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java) * disjointsetunion * [DisjointSetUnion](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnion.java) * [Node](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/disjointsetunion/Node.java) @@ -617,6 +618,7 @@ * [MRUCacheTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/caches/MRUCacheTest.java) * crdt * [GCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/GCounterTest.java) + * [PNCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java) * disjointsetunion * [DisjointSetUnionTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionTest.java) * graphs diff --git a/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java b/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java new file mode 100644 index 000000000..828e0b080 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java @@ -0,0 +1,100 @@ +package com.thealgorithms.datastructures.crdt; + +import java.util.HashMap; +import java.util.Map; + +/** + * PN-Counter (Positive-Negative Counter) is a state-based CRDT (Conflict-free Replicated Data Type) + * designed for tracking counts with both increments and decrements in a distributed and concurrent environment. + * It combines two G-Counters, one for increments (P) and one for decrements (N). + * The total count is obtained by subtracting the value of the decrement counter from the increment counter. + * This implementation supports incrementing, decrementing, querying the total count, + * comparing with other PN-Counters, and merging with another PN-Counter + * to compute the element-wise maximum for both increment and decrement counters. + * (https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) + * + * @author itakurah (Niklas Hoefflin) (https://github.com/itakurah) + */ + +class PNCounter { + private final Map P; + private final Map N; + private final int myId; + private final int n; + + /** + * Constructs a PN-Counter for a cluster of n nodes. + * + * @param myId The identifier of the current node. + * @param n The number of nodes in the cluster. + */ + public PNCounter(int myId, int n) { + this.myId = myId; + this.n = n; + this.P = new HashMap<>(); + this.N = new HashMap<>(); + + for (int i = 0; i < n; i++) { + P.put(i, 0); + N.put(i, 0); + } + } + + /** + * Increments the increment counter for the current node. + */ + public void increment() { + P.put(myId, P.get(myId) + 1); + } + + /** + * Increments the decrement counter for the current node. + */ + public void decrement() { + N.put(myId, N.get(myId) + 1); + } + + /** + * Gets the total value of the counter by subtracting the decrement counter from the increment counter. + * + * @return The total value of the counter. + */ + public int value() { + int sumP = P.values().stream().mapToInt(Integer::intValue).sum(); + int sumN = N.values().stream().mapToInt(Integer::intValue).sum(); + return sumP - sumN; + } + + /** + * Compares the state of this PN-Counter with another PN-Counter. + * + * @param other The other PN-Counter to compare with. + * @return True if the state of this PN-Counter is less than or equal to the state of the other PN-Counter. + */ + public boolean compare(PNCounter other) { + if (this.n != other.n) { + throw new IllegalArgumentException("Cannot compare PN-Counters with different number of nodes"); + } + for (int i = 0; i < n; i++) { + if (this.P.get(i) > other.P.get(i) && this.N.get(i) > other.N.get(i)) { + return false; + } + } + return true; + } + + /** + * Merges the state of this PN-Counter with another PN-Counter. + * + * @param other The other PN-Counter to merge with. + */ + public void merge(PNCounter other) { + if (this.n != other.n) { + throw new IllegalArgumentException("Cannot merge PN-Counters with different number of nodes"); + } + for (int i = 0; i < n; i++) { + this.P.put(i, Math.max(this.P.get(i), other.P.get(i))); + this.N.put(i, Math.max(this.N.get(i), other.N.get(i))); + } + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java b/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java new file mode 100644 index 000000000..46c22a6ed --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.datastructures.crdt; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class PNCounterTest { + + @Test + public void testIncrement() { + PNCounter counter = new PNCounter(0, 3); + counter.increment(); + assertEquals(1, counter.value()); + } + + @Test + public void testDecrement() { + PNCounter counter = new PNCounter(0, 3); + counter.decrement(); + assertEquals(-1, counter.value()); + } + + @Test + public void testIncrementAndDecrement() { + PNCounter counter = new PNCounter(0, 3); + counter.increment(); + counter.increment(); + counter.decrement(); + assertEquals(1, counter.value()); + } + + @Test + public void testCompare() { + PNCounter counter1 = new PNCounter(0, 3); + counter1.increment(); + PNCounter counter2 = new PNCounter(1, 3); + assertTrue(counter1.compare(counter2)); + counter2.increment(); + assertTrue(counter2.compare(counter1)); + counter1.decrement(); + assertFalse(counter1.compare(counter2)); + } + + @Test + public void testMerge() { + PNCounter counter1 = new PNCounter(0, 3); + counter1.increment(); + counter1.increment(); + PNCounter counter2 = new PNCounter(1, 3); + counter2.increment(); + counter1.merge(counter2); + assertEquals(3, counter1.value()); + } +}