From e26fd9da71130837d327d747f6bc87688322eee5 Mon Sep 17 00:00:00 2001
From: Niklas Hoefflin <122729995+itakurah@users.noreply.github.com>
Date: Mon, 11 Dec 2023 22:05:43 +0100
Subject: [PATCH] Add OR-Set (Observed-Remove Set) (#4980)
---
DIRECTORY.md | 2 +
.../datastructures/crdt/ORSet.java | 191 ++++++++++++++++++
.../datastructures/crdt/ORSetTest.java | 86 ++++++++
3 files changed, 279 insertions(+)
create mode 100644 src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java
create mode 100644 src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.java
diff --git a/DIRECTORY.md b/DIRECTORY.md
index f033416d..b769250e 100644
--- a/DIRECTORY.md
+++ b/DIRECTORY.md
@@ -86,6 +86,7 @@
* [GCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/GCounter.java)
* [GSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/GSet.java)
* [LWWElementSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/LWWElementSet.java)
+ * [ORSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java)
* [PNCounter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/PNCounter.java)
* [TwoPSet](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/crdt/TwoPSet.java)
* disjointsetunion
@@ -623,6 +624,7 @@
* [GCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/GCounterTest.java)
* [GSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/GSetTest.java)
* [LWWElementSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/LWWElementSetTest.java)
+ * [ORSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.java)
* [PNCounterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/PNCounterTest.java)
* [TwoPSetTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/crdt/TwoPSetTest.java)
* disjointsetunion
diff --git a/src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java b/src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java
new file mode 100644
index 00000000..a4cc2ffd
--- /dev/null
+++ b/src/main/java/com/thealgorithms/datastructures/crdt/ORSet.java
@@ -0,0 +1,191 @@
+package com.thealgorithms.datastructures.crdt;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * ORSet (Observed-Removed Set) is a state-based CRDT (Conflict-free Replicated Data Type)
+ * that supports both addition and removal of elements. This particular implementation follows
+ * the Add-Wins strategy, meaning that in case of conflicting add and remove operations,
+ * the add operation takes precedence. The merge operation of two OR-Sets ensures that
+ * elements added at any replica are eventually observed at all replicas. Removed elements,
+ * once observed, are never reintroduced.
+ * This OR-Set implementation provides methods for adding elements, removing elements,
+ * checking for element existence, retrieving the set of elements, comparing with other OR-Sets,
+ * and merging with another OR-Set to create a new OR-Set containing all unique elements
+ * from both sets.
+ *
+ * @author itakurah (Niklas Hoefflin) (https://github.com/itakurah)
+ * @see Conflict-free_replicated_data_type
+ * @see itakurah (Niklas Hoefflin)
+ */
+
+public class ORSet {
+
+ private final Set> elements;
+ private final Set> tombstones;
+
+ /**
+ * Constructs an empty OR-Set.
+ */
+ public ORSet() {
+ this.elements = new HashSet<>();
+ this.tombstones = new HashSet<>();
+ }
+
+ /**
+ * Checks if the set contains the specified element.
+ *
+ * @param element the element to check for
+ * @return true if the set contains the element, false otherwise
+ */
+ public boolean contains(T element) {
+ return elements.stream().anyMatch(pair -> pair.getElement().equals(element));
+ }
+
+ /**
+ * Retrieves the elements in the set.
+ *
+ * @return a set containing the elements
+ */
+ public Set elements() {
+ Set result = new HashSet<>();
+ elements.forEach(pair -> result.add(pair.getElement()));
+ return result;
+ }
+
+ /**
+ * Adds the specified element to the set.
+ *
+ * @param element the element to add
+ */
+ public void add(T element) {
+ String n = prepare();
+ effect(element, n);
+ }
+
+ /**
+ * Removes the specified element from the set.
+ *
+ * @param element the element to remove
+ */
+ public void remove(T element) {
+ Set> pairsToRemove = prepare(element);
+ effect(pairsToRemove);
+ }
+
+ /**
+ * Collect all pairs with the specified element.
+ *
+ * @param element the element to collect pairs for
+ * @return a set of pairs with the specified element to be removed
+ */
+ private Set> prepare(T element) {
+ Set> pairsToRemove = new HashSet<>();
+ for (Pair pair : elements) {
+ if (pair.getElement().equals(element)) {
+ pairsToRemove.add(pair);
+ }
+ }
+ return pairsToRemove;
+ }
+
+ /**
+ * Generates a unique tag for the element.
+ *
+ * @return the unique tag
+ */
+ private String prepare() {
+ return generateUniqueTag();
+ }
+
+ /**
+ * Adds the element with the specified unique tag to the set.
+ *
+ * @param element the element to add
+ * @param n the unique tag associated with the element
+ */
+ private void effect(T element, String n) {
+ Pair pair = new Pair<>(element, n);
+ elements.add(pair);
+ elements.removeAll(tombstones);
+ }
+
+ /**
+ * Removes the specified pairs from the set.
+ *
+ * @param pairsToRemove the pairs to remove
+ */
+ private void effect(Set> pairsToRemove) {
+ elements.removeAll(pairsToRemove);
+ tombstones.addAll(pairsToRemove);
+ }
+
+ /**
+ * Generates a unique tag.
+ *
+ * @return the unique tag
+ */
+ private String generateUniqueTag() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * Compares this Add-Wins OR-Set with another OR-Set to check if elements and tombstones are a subset.
+ *
+ * @param other the other OR-Set to compare
+ * @return true if the sets are subset, false otherwise
+ */
+ public boolean compare(ORSet other) {
+ Set> union = new HashSet<>(elements);
+ union.addAll(tombstones);
+
+ Set> otherUnion = new HashSet<>(other.elements);
+ otherUnion.addAll(other.tombstones);
+
+ return otherUnion.containsAll(union) && other.tombstones.containsAll(tombstones);
+ }
+
+ /**
+ * Merges this Add-Wins OR-Set with another OR-Set.
+ *
+ * @param other the other OR-Set to merge
+ */
+ public void merge(ORSet other) {
+ elements.removeAll(other.tombstones);
+ other.elements.removeAll(tombstones);
+ elements.addAll(other.elements);
+ tombstones.addAll(other.tombstones);
+ }
+
+ /**
+ * Represents a pair containing an element and a unique tag.
+ *
+ * @param the type of the element in the pair
+ */
+ public static class Pair {
+ private final T element;
+ private final String uniqueTag;
+
+ /**
+ * Constructs a pair with the specified element and unique tag.
+ *
+ * @param element the element in the pair
+ * @param uniqueTag the unique tag associated with the element
+ */
+ public Pair(T element, String uniqueTag) {
+ this.element = element;
+ this.uniqueTag = uniqueTag;
+ }
+
+ /**
+ * Gets the element from the pair.
+ *
+ * @return the element
+ */
+ public T getElement() {
+ return element;
+ }
+ }
+}
diff --git a/src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.java b/src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.java
new file mode 100644
index 00000000..f12c38f1
--- /dev/null
+++ b/src/test/java/com/thealgorithms/datastructures/crdt/ORSetTest.java
@@ -0,0 +1,86 @@
+package com.thealgorithms.datastructures.crdt;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class ORSetTest {
+
+ @Test
+ void testContains() {
+ ORSet orSet = new ORSet<>();
+ orSet.add("A");
+ assertTrue(orSet.contains("A"));
+ }
+
+ @Test
+ void testAdd() {
+ ORSet orSet = new ORSet<>();
+ orSet.add("A");
+ assertTrue(orSet.contains("A"));
+ }
+
+ @Test
+ void testRemove() {
+ ORSet orSet = new ORSet<>();
+ orSet.add("A");
+ orSet.add("A");
+ orSet.remove("A");
+ assertFalse(orSet.contains("A"));
+ }
+
+ @Test
+ void testElements() {
+ ORSet orSet = new ORSet<>();
+ orSet.add("A");
+ orSet.add("B");
+ assertEquals(Set.of("A", "B"), orSet.elements());
+ }
+
+ @Test
+ void testCompareEqualSets() {
+ ORSet orSet1 = new ORSet<>();
+ ORSet orSet2 = new ORSet<>();
+
+ orSet1.add("A");
+ orSet2.add("A");
+ orSet2.add("B");
+ orSet2.add("C");
+ orSet2.remove("C");
+ orSet1.merge(orSet2);
+ orSet2.merge(orSet1);
+ orSet2.remove("B");
+
+ assertTrue(orSet1.compare(orSet2));
+ }
+
+ @Test
+ void testCompareDifferentSets() {
+ ORSet orSet1 = new ORSet<>();
+ ORSet orSet2 = new ORSet<>();
+
+ orSet1.add("A");
+ orSet2.add("B");
+
+ assertFalse(orSet1.compare(orSet2));
+ }
+
+ @Test
+ void testMerge() {
+ ORSet orSet1 = new ORSet<>();
+ ORSet orSet2 = new ORSet<>();
+
+ orSet1.add("A");
+ orSet1.add("A");
+ orSet1.add("B");
+ orSet1.remove("B");
+ orSet2.add("B");
+ orSet2.add("C");
+ orSet2.remove("C");
+ orSet1.merge(orSet2);
+
+ assertTrue(orSet1.contains("A"));
+ assertTrue(orSet1.contains("B"));
+ }
+}