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")); + } +}