From f1b5d5e6dbcd9fa19fafa419300a9ffbfefa02d8 Mon Sep 17 00:00:00 2001 From: Neil C Smith Date: Fri, 17 May 2024 16:41:04 +0100 Subject: [PATCH] Add OrderedSet interface and implementation, and extend OrderedMap to implement SequencedMap. --- .../java/org/praxislive/core/OrderedMap.java | 10 +- .../org/praxislive/core/OrderedMapImpl.java | 41 ++-- .../java/org/praxislive/core/OrderedSet.java | 184 ++++++++++++++++++ .../org/praxislive/core/OrderedSetImpl.java | 132 +++++++++++++ .../org/praxislive/core/OrderedMapTest.java | 10 +- .../org/praxislive/core/OrderedSetTest.java | 116 +++++++++++ 6 files changed, 470 insertions(+), 23 deletions(-) create mode 100644 praxiscore-api/src/main/java/org/praxislive/core/OrderedSet.java create mode 100644 praxiscore-api/src/main/java/org/praxislive/core/OrderedSetImpl.java create mode 100644 praxiscore-api/src/test/java/org/praxislive/core/OrderedSetTest.java diff --git a/praxiscore-api/src/main/java/org/praxislive/core/OrderedMap.java b/praxiscore-api/src/main/java/org/praxislive/core/OrderedMap.java index 1db10a77..137cb500 100644 --- a/praxiscore-api/src/main/java/org/praxislive/core/OrderedMap.java +++ b/praxiscore-api/src/main/java/org/praxislive/core/OrderedMap.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2023 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; +import java.util.SequencedMap; import java.util.stream.Stream; /** @@ -32,7 +33,7 @@ * @param key type * @param value type */ -public sealed interface OrderedMap extends Map permits OrderedMapImpl { +public sealed interface OrderedMap extends SequencedMap permits OrderedMapImpl { /** * A {@link List} containing the keys of this OrderedMap. @@ -47,6 +48,9 @@ public sealed interface OrderedMap extends Map permits OrderedMapImp */ public List keys(); + @Override + public OrderedMap reversed(); + /** * Compares the provided object with this map for equality in accordance * with the specification of {@link Map#equals(java.lang.Object)}. @@ -204,7 +208,7 @@ public static OrderedMap ofEntries(Map.Entry OrderedMap copyOf(Map map) { + public static OrderedMap copyOf(Map map) { if (map instanceof OrderedMapImpl) { return (OrderedMap) map; } else { diff --git a/praxiscore-api/src/main/java/org/praxislive/core/OrderedMapImpl.java b/praxiscore-api/src/main/java/org/praxislive/core/OrderedMapImpl.java index 4da24349..98ac99e2 100644 --- a/praxiscore-api/src/main/java/org/praxislive/core/OrderedMapImpl.java +++ b/praxiscore-api/src/main/java/org/praxislive/core/OrderedMapImpl.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2023 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -48,11 +48,6 @@ public List keys() { return keys; } - @Override - public void clear() { - map.clear(); - } - @Override public boolean containsKey(Object key) { return map.containsKey(key); @@ -116,18 +111,8 @@ public int size() { } @Override - public V put(K key, V value) { - throw new UnsupportedOperationException(); - } - - @Override - public void putAll(Map m) { - throw new UnsupportedOperationException(); - } - - @Override - public V remove(Object key) { - throw new UnsupportedOperationException(); + public OrderedMap reversed() { + return new OrderedMapImpl<>(List.copyOf(keys.reversed()), map); } @Override @@ -178,4 +163,24 @@ public String toString() { .collect(Collectors.joining(", ", "{", "}")); } + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + } diff --git a/praxiscore-api/src/main/java/org/praxislive/core/OrderedSet.java b/praxiscore-api/src/main/java/org/praxislive/core/OrderedSet.java new file mode 100644 index 00000000..99e5900e --- /dev/null +++ b/praxiscore-api/src/main/java/org/praxislive/core/OrderedSet.java @@ -0,0 +1,184 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.core; + +import java.util.List; +import java.util.Set; +import java.util.SequencedSet; + +/** + * A {@link Set} with consistent order of entries. All static factory methods + * produce unmodifiable sets. + * + * @param value type + */ +public sealed interface OrderedSet extends SequencedSet permits OrderedSetImpl { + + /** + * A {@link List} containing all the values of this OrderedSet. + *

+ * Because Set equality ignores order, code needing to verify whether an + * OrderedSet contains the same values in the same order should also check + * for equality of the values list. + * + * @return values as list + */ + public List values(); + + @Override + public OrderedSet reversed(); + + /** + * Compares the provided object with this map for equality in accordance + * with the specification of {@link Map#equals(java.lang.Object)}. + *

+ * Because Map equality does not depends on order, code needing to verify + * whether an OrderedMap contains the same mappings in the same order should + * also check for equality of the {@link #keys()} list. + * + * @param obj object to compare + * @return true if the object is a map containing the same mappings, not + * necessarily in the same order + */ + @Override + public boolean equals(Object obj); + + /** + * Returns an empty OrderedSet. + * + * @param value type + * @return empty OrderedSet + */ + public static OrderedSet of() { + return new OrderedSetImpl<>(Set.of(), List.of()); + } + + /** + * Returns an OrderedSet with a single element. + * + * @param element type + * @param element element + * @return an OrderedSet of the provided element + * @throws NullPointerException if element is null + */ + public static OrderedSet of(E element) { + return new OrderedSetImpl<>(Set.of(element), List.of(element)); + } + + /** + * Returns an OrderedSet with two elements. + * + * @param element type + * @param e1 first element + * @param e2 second element + * @return an OrderedSet of the provided elements + * @throws NullPointerException if any elements are null + * @throws IllegalArgumentException if any elements are duplicated + */ + public static OrderedSet of(E e1, E e2) { + return new OrderedSetImpl<>(Set.of(e1, e2), List.of(e1, e2)); + } + + /** + * Returns an OrderedSet with three elements. + * + * @param element type + * @param e1 first element + * @param e2 second element + * @param e3 third element + * @return an OrderedSet of the provided elements + * @throws NullPointerException if any elements are null + * @throws IllegalArgumentException if any elements are duplicated + */ + public static OrderedSet of(E e1, E e2, E e3) { + return new OrderedSetImpl<>(Set.of(e1, e2, e3), List.of(e1, e2, e3)); + } + + /** + * Returns an OrderedSet with four elements. + * + * @param element type + * @param e1 first element + * @param e2 second element + * @param e3 third element + * @param e4 fourth element + * @return an OrderedSet of the provided elements + * @throws NullPointerException if any elements are null + * @throws IllegalArgumentException if any elements are duplicated + */ + public static OrderedSet of(E e1, E e2, E e3, E e4) { + return new OrderedSetImpl<>(Set.of(e1, e2, e3, e4), List.of(e1, e2, e3, e4)); + } + + /** + * Returns an OrderedSet with five elements. + * + * @param element type + * @param e1 first element + * @param e2 second element + * @param e3 third element + * @param e4 fourth element + * @param e5 fifth element + * @return an OrderedSet of the provided elements + * @throws NullPointerException if any elements are null + * @throws IllegalArgumentException if any elements are duplicated + */ + public static OrderedSet of(E e1, E e2, E e3, E e4, E e5) { + return new OrderedSetImpl<>(Set.of(e1, e2, e3, e4, e5), List.of(e1, e2, e3, e4, e5)); + } + + /** + * Returns an OrderedSet of the provided elements. + * + * @param element type + * @param elements elements + * @return an OrderedSet of the provided elements + * @throws c + * @throws IllegalArgumentException if any elements are duplicated + */ + @SafeVarargs + @SuppressWarnings("varargs") + public static OrderedSet of(E... elements) { + return new OrderedSetImpl<>(Set.of(elements), List.of(elements)); + } + + /** + * Returns an unmodifiable OrderedSet that contains a copy of the provided + * Set. The order will be the same as the iteration order of the set. + *

+ * If the set is already an unmodifiable OrderedSet it may be returned as + * is. + * + * @param element type + * @param set set to copy + * @return an ordered set copy of the provided set + * @throws NullPointerException if any element is null or the set is null + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static OrderedSet copyOf(Set set) { + if (set instanceof OrderedSetImpl) { + return (OrderedSet) set; + } else { + return (OrderedSet) OrderedSet.of(set.toArray()); + } + } +} diff --git a/praxiscore-api/src/main/java/org/praxislive/core/OrderedSetImpl.java b/praxiscore-api/src/main/java/org/praxislive/core/OrderedSetImpl.java new file mode 100644 index 00000000..44874f71 --- /dev/null +++ b/praxiscore-api/src/main/java/org/praxislive/core/OrderedSetImpl.java @@ -0,0 +1,132 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.core; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * + */ +final class OrderedSetImpl implements OrderedSet { + + private final Set set; + private final List list; + + OrderedSetImpl(Set set, List list) { + this.set = set; + this.list = list; + } + + @Override + public boolean contains(Object o) { + return set.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return set.containsAll(c); + } + + @Override + public boolean isEmpty() { + return set.isEmpty(); + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public OrderedSet reversed() { + return new OrderedSetImpl<>(set, list.reversed()); + } + + @Override + public int size() { + return set.size(); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public List values() { + return list; + } + + @Override + public int hashCode() { + return set.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj == this || set.equals(obj); + } + + @Override + public String toString() { + return list.toString(); + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + +} diff --git a/praxiscore-api/src/test/java/org/praxislive/core/OrderedMapTest.java b/praxiscore-api/src/test/java/org/praxislive/core/OrderedMapTest.java index f30d567d..d737171f 100644 --- a/praxiscore-api/src/test/java/org/praxislive/core/OrderedMapTest.java +++ b/praxiscore-api/src/test/java/org/praxislive/core/OrderedMapTest.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2023 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -86,7 +86,13 @@ public void testOrdering() { .map(Map.Entry::getValue) .toList(); assertEquals(List.of(v1, v2, v3), values); - + var reversed = map.reversed(); + assertEquals(map, reversed); + assertEquals(List.of(k3, k2, k1), reversed.keys()); + values = reversed.entrySet().stream() + .map(Map.Entry::getValue) + .toList(); + assertEquals(List.of(v3, v2, v1), values); } @Test diff --git a/praxiscore-api/src/test/java/org/praxislive/core/OrderedSetTest.java b/praxiscore-api/src/test/java/org/praxislive/core/OrderedSetTest.java new file mode 100644 index 00000000..1c4bc1f9 --- /dev/null +++ b/praxiscore-api/src/test/java/org/praxislive/core/OrderedSetTest.java @@ -0,0 +1,116 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2024 Neil C Smith. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License version 3 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * version 3 for more details. + * + * You should have received a copy of the GNU Lesser General Public License version 3 + * along with this work; if not, see http://www.gnu.org/licenses/ + * + * + * Please visit https://www.praxislive.org if you need additional information or + * have any questions. + */ +package org.praxislive.core; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * + */ +public class OrderedSetTest { + + private final String e1 = "ELEMENT ONE"; + private final String e2 = "ELEMENT TWO"; + private final String e3 = "ELEMENT THREE"; + private final String e4 = "ELEMENT FOUR"; + private final String e5 = "ELEMENT FIVE"; + private final String e6 = "ELEMENT SIX"; + + @Test + public void testEquality() { + var set = Set.of(e1, e2, e3, e4, e5, e6); + var os1 = OrderedSet.of(e1, e2, e3, e4, e5, e6); + var os2 = OrderedSet.of(e6, e5, e4, e3, e2, e1); + + assertEquals(set, os1); + assertEquals(set, os2); + assertEquals(os1, os2); + + assertNotEquals(os1.values(), os2.values()); + assertEquals(os1.values(), os2.reversed().values()); + } + + @Test + public void testOrdering() { + var set = OrderedSet.of(e1, e2, e3, e4); + var values = set.stream().toList(); + assertEquals(List.of(e1, e2, e3, e4), values); + var reversed = set.reversed(); + assertEquals(set, reversed); + assertEquals(List.of(e4, e3, e2, e1), reversed.values()); + values = reversed.stream().toList(); + assertEquals(List.of(e4, e3, e2, e1), values); + } + + @Test + public void testOrderingOfCopy() { + var lhs = new LinkedHashSet(); + lhs.addAll(List.of(e1, e2, e3, e4, e5, e6)); + var set = OrderedSet.copyOf(lhs); + assertEquals(lhs, set); + assertEquals(set.values(), lhs.stream().toList()); + var reversed = set.reversed(); + var checkCopy = OrderedSet.copyOf(reversed); + assertSame(reversed, checkCopy); + assertNotEquals(reversed.values(), lhs.stream().toList()); + lhs.clear(); + lhs.addAll(reversed); + assertEquals(reversed, lhs); + assertEquals(reversed.values(), lhs.stream().toList()); + } + + @Test + public void testImmutability() { + var set = OrderedSet.of(e3, e4, e5); + assertThrows(UnsupportedOperationException.class, set::clear); + assertThrows(UnsupportedOperationException.class, () -> set.add(e2)); + assertThrows(UnsupportedOperationException.class, () -> set.addFirst(e1)); + assertThrows(UnsupportedOperationException.class, () -> set.addLast(e6)); + assertThrows(UnsupportedOperationException.class, () -> set.addAll(List.of(e1, e2, e6))); + assertThrows(UnsupportedOperationException.class, () -> set.remove(e4)); + assertThrows(UnsupportedOperationException.class, () -> set.removeAll(List.of(e2, e3))); + assertThrows(UnsupportedOperationException.class, () -> set.values().add(e6)); + assertThrows(UnsupportedOperationException.class, () -> set.values().clear()); + assertThrows(UnsupportedOperationException.class, () -> { + var itr = set.iterator(); + itr.next(); + itr.remove(); + }); + assertThrows(UnsupportedOperationException.class, () -> { + var itr = set.values().iterator(); + itr.next(); + itr.remove(); + }); + } + +}