diff --git a/ax-adt/pom.xml b/ax-adt/pom.xml index 0c91b9d8..baa8076a 100644 --- a/ax-adt/pom.xml +++ b/ax-adt/pom.xml @@ -22,7 +22,7 @@ com.g2forge.alexandria - ax-collection + ax-path ${project.version} diff --git a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/ITrie.java b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/ITrie.java new file mode 100644 index 00000000..a89e7e3d --- /dev/null +++ b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/ITrie.java @@ -0,0 +1,8 @@ +package com.g2forge.alexandria.adt.trie; + +import com.g2forge.alexandria.java.fluent.optional.IOptional; +import com.g2forge.alexandria.path.path.IPath; + +public interface ITrie { + public IOptional get(IPath path); +} diff --git a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java index 3f7146e0..7581d706 100644 --- a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java +++ b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java @@ -1,5 +1,33 @@ package com.g2forge.alexandria.adt.trie; -import com.g2forge.alexandria.adt.graph.v2.member.ASingleGraphMember; +import java.util.HashMap; +import java.util.Map; -public class Node extends ASingleGraphMember {} +import com.g2forge.alexandria.path.path.IPath; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@AllArgsConstructor +@ToString +@EqualsAndHashCode +public class Node { + protected final IPath label; + + protected final Map> children; + + protected boolean isTerminal; + + protected V value; + + public Node(IPath label) { + this(label, new HashMap<>(), false, null); + } + + public Node(IPath label, V value) { + this(label, new HashMap<>(), true, value); + } +} diff --git a/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Trie.java b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Trie.java new file mode 100644 index 00000000..800a46c9 --- /dev/null +++ b/ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Trie.java @@ -0,0 +1,48 @@ +package com.g2forge.alexandria.adt.trie; + +import com.g2forge.alexandria.java.fluent.optional.IOptional; +import com.g2forge.alexandria.java.fluent.optional.NullableOptional; +import com.g2forge.alexandria.path.path.IPath; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Getter(AccessLevel.PROTECTED) +public class Trie implements ITrie { + protected Node root; + + @Override + public IOptional get(IPath path) { + final Node root = getRoot(); + Node current; + + // Create an artificial parent of the root, if the root is labeled + if ((root.getLabel() != null) && !root.getLabel().isEmpty()) { + current = new Node<>(null); + current.getChildren().put(getRoot().getLabel().getFirst(), getRoot()); + } else current = root; + + int index = 0; + while (index < path.size()) { + // Find the child + final Node next = current.getChildren().get(path.getComponent(index)); + if (next == null) return NullableOptional.empty(); + + // Ensure the label on the next node isn't longer than the path + final int nextLabelSize = next.getLabel().size(); + if (path.size() < (index + nextLabelSize)) return NullableOptional.empty(); + + // Ensure the label on the next node matches the path + final IPath subPath = path.subPath(index, index + nextLabelSize); + if (!subPath.equals(next.getLabel())) return NullableOptional.empty(); + + current = next; + index += nextLabelSize; + } + + return current.isTerminal() ? NullableOptional.of(current.getValue()) : NullableOptional.empty(); + } + +} diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/NodeBuilder.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/NodeBuilder.java new file mode 100644 index 00000000..3f994bee --- /dev/null +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/NodeBuilder.java @@ -0,0 +1,41 @@ +package com.g2forge.alexandria.adt.trie; + +import com.g2forge.alexandria.java.core.helpers.HArray; +import com.g2forge.alexandria.java.function.IConsumer1; +import com.g2forge.alexandria.java.function.builder.IBuilder; +import com.g2forge.alexandria.path.path.IPath; +import com.g2forge.alexandria.path.path.Path; + +public class NodeBuilder implements IBuilder> { + public interface IChildBuilder { + public NodeBuilder child(String label, String value); + } + + public static IPath toLabel(String label) { + return new Path<>(HArray.toObject(label.toCharArray())); + } + + protected final Node node; + + public NodeBuilder(String label, String value) { + if (value == null) this.node = new Node<>(toLabel(label)); + else this.node = new Node<>(toLabel(label), value); + } + + @Override + public Node build() { + return node; + } + + public NodeBuilder children(IConsumer1 consumer) { + consumer.accept(new IChildBuilder() { + @Override + public NodeBuilder child(String label, String value) { + final NodeBuilder retVal = new NodeBuilder(label, value); + node.getChildren().put(retVal.node.getLabel().getFirst(), retVal.node); + return retVal; + } + }); + return this; + } +} \ No newline at end of file diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNodeBuilder.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNodeBuilder.java new file mode 100644 index 00000000..28319c41 --- /dev/null +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestNodeBuilder.java @@ -0,0 +1,25 @@ +package com.g2forge.alexandria.adt.trie; + +import org.junit.Test; + +import com.g2forge.alexandria.path.path.Path; +import com.g2forge.alexandria.test.HAssert; + +public class TestNodeBuilder { + @Test + public void root() { + HAssert.assertEquals(new Node<>(new Path()), new NodeBuilder("", null).build());;; + } + + @Test + public void label() { + HAssert.assertEquals(new Node<>(new Path('a', 'b', 'c')), new NodeBuilder("abc", null).build());;; + } + + @Test + public void child() { + final Node expected = new Node<>(new Path('a', 'b', 'c')); + expected.getChildren().put('d', new Node<>(new Path('d'), "value")); + HAssert.assertEquals(expected, new NodeBuilder("abc", null).children(c -> c.child("d", "value")).build());;; + } +} diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java new file mode 100644 index 00000000..627903ff --- /dev/null +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie3Node.java @@ -0,0 +1,63 @@ +package com.g2forge.alexandria.adt.trie; + +import org.junit.Test; + +import com.g2forge.alexandria.java.fluent.optional.IOptional; +import com.g2forge.alexandria.test.HAssert; + +public class TestTrie3Node { + protected static final ITrie trie = new Trie<>(new NodeBuilder("t", null).children(c -> { + c.child("est", "test"); + c.child("oast", "toast"); + }).build()); + + @Test + public void roast() { + final IOptional result = trie.get(NodeBuilder.toLabel("roast")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void temp() { + final IOptional result = trie.get(NodeBuilder.toLabel("temp")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void test() { + final IOptional result = trie.get(NodeBuilder.toLabel("test")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("test", result.get()); + } + + @Test + public void toast() { + final IOptional result = trie.get(NodeBuilder.toLabel("toast")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("toast", result.get()); + } + + @Test + public void toaster() { + final IOptional result = trie.get(NodeBuilder.toLabel("toaster")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void toasting() { + final IOptional result = trie.get(NodeBuilder.toLabel("toasting")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void toasti() { + final IOptional result = trie.get(NodeBuilder.toLabel("toasti")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void trip() { + final IOptional result = trie.get(NodeBuilder.toLabel("trip")); + HAssert.assertFalse(result.isNotEmpty()); + } +} diff --git a/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java new file mode 100644 index 00000000..40661414 --- /dev/null +++ b/ax-adt/src/test/java/com/g2forge/alexandria/adt/trie/TestTrie5Node.java @@ -0,0 +1,68 @@ +package com.g2forge.alexandria.adt.trie; + +import org.junit.Test; + +import com.g2forge.alexandria.java.fluent.optional.IOptional; +import com.g2forge.alexandria.test.HAssert; + +public class TestTrie5Node { + protected static final ITrie trie = new Trie<>(new NodeBuilder("t", null).children(c0 -> { + c0.child("est", "test"); + c0.child("oast", "toast").children(c1 -> { + c1.child("er", "toaster"); + c1.child("ing", "toasting"); + }); + }).build()); + + @Test + public void roast() { + final IOptional result = trie.get(NodeBuilder.toLabel("roast")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void temp() { + final IOptional result = trie.get(NodeBuilder.toLabel("temp")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void test() { + final IOptional result = trie.get(NodeBuilder.toLabel("test")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("test", result.get()); + } + + @Test + public void toast() { + final IOptional result = trie.get(NodeBuilder.toLabel("toast")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("toast", result.get()); + } + + @Test + public void toaster() { + final IOptional result = trie.get(NodeBuilder.toLabel("toaster")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("toaster", result.get()); + } + + @Test + public void toasti() { + final IOptional result = trie.get(NodeBuilder.toLabel("toasti")); + HAssert.assertFalse(result.isNotEmpty()); + } + + @Test + public void toasting() { + final IOptional result = trie.get(NodeBuilder.toLabel("toasting")); + HAssert.assertTrue(result.isNotEmpty()); + HAssert.assertEquals("toasting", result.get()); + } + + @Test + public void trip() { + final IOptional result = trie.get(NodeBuilder.toLabel("trip")); + HAssert.assertFalse(result.isNotEmpty()); + } +} diff --git a/ax-java/src/main/java/com/g2forge/alexandria/java/core/helpers/HArray.java b/ax-java/src/main/java/com/g2forge/alexandria/java/core/helpers/HArray.java index 3b673a8b..69904c90 100644 --- a/ax-java/src/main/java/com/g2forge/alexandria/java/core/helpers/HArray.java +++ b/ax-java/src/main/java/com/g2forge/alexandria/java/core/helpers/HArray.java @@ -14,11 +14,6 @@ @Helpers @UtilityClass public class HArray { - @SafeVarargs - public static T[] create(T... array) { - return array; - } - @SuppressWarnings("unchecked") public static O[] cast(Class type, I... array) { final Object retVal = Array.newInstance(type, array.length); @@ -48,6 +43,11 @@ public static boolean contains(T value, T... array) { return false; } + @SafeVarargs + public static T[] create(T... array) { + return array; + } + @SafeVarargs public static B[] map(final Class type, final Function function, final A... values) { @SuppressWarnings("unchecked") @@ -64,4 +64,53 @@ public static List map(final Function function retVal.add(function.apply(values[i])); return retVal; } + + public static Byte[] toObject(byte[] array) { + final Byte[] retVal = new Byte[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Character[] toObject(char[] array) { + final Character[] retVal = new Character[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Double[] toObject(double[] array) { + final Double[] retVal = new Double[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Float[] toObject(float[] array) { + final Float[] retVal = new Float[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Integer[] toObject(int[] array) { + final Integer[] retVal = new Integer[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Long[] toObject(long[] array) { + final Long[] retVal = new Long[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } + + public static Short[] toObject(short[] array) { + final Short[] retVal = new Short[array.length]; + for (int i = 0; i < array.length; i++) + retVal[i] = array[i]; + return retVal; + } } diff --git a/ax-path/src/main/java/com/g2forge/alexandria/path/path/IPath.java b/ax-path/src/main/java/com/g2forge/alexandria/path/path/IPath.java index 0173ca7a..398770d4 100644 --- a/ax-path/src/main/java/com/g2forge/alexandria/path/path/IPath.java +++ b/ax-path/src/main/java/com/g2forge/alexandria/path/path/IPath.java @@ -52,6 +52,16 @@ public default T getLast() { public IPath getParent(); + /** + * Extract a portion of this path. Negative indices are measured from the end, with {@code -2} indicating the last component of the path. This means + * {@code subPath(-2, -1)} returns a path consisting only of the last component. + * + * @param fromIndex The index of the first component to include (inclusive). + * @param toIndex The index of the first component to exclude (exclusive). + * @return A portion of this path. + */ + public IPath subPath(int fromIndex, int toIndex); + public default boolean isEmpty() { return getComponents().isEmpty(); } diff --git a/ax-path/src/main/java/com/g2forge/alexandria/path/path/Path.java b/ax-path/src/main/java/com/g2forge/alexandria/path/path/Path.java index 0b8b0016..131820b5 100644 --- a/ax-path/src/main/java/com/g2forge/alexandria/path/path/Path.java +++ b/ax-path/src/main/java/com/g2forge/alexandria/path/path/Path.java @@ -56,4 +56,12 @@ public IPath resolve(IPath subpath) { if (subpath.isEmpty()) return this; return create(HCollection.concatenate(getComponents().toCollection(), subpath.getComponents().toCollection())); } + + @Override + public IPath subPath(int fromIndex, int toIndex) { + if (fromIndex < 0) fromIndex += size() + 1; + if (toIndex < 0) toIndex += size() + 1; + final List list = HCollection.asList(getComponents().toCollection()); + return create(list.subList(fromIndex, toIndex)); + } } diff --git a/ax-path/src/test/java/com/g2forge/alexandria/path/path/TestPath.java b/ax-path/src/test/java/com/g2forge/alexandria/path/path/TestPath.java index 732e72dc..af69a047 100644 --- a/ax-path/src/test/java/com/g2forge/alexandria/path/path/TestPath.java +++ b/ax-path/src/test/java/com/g2forge/alexandria/path/path/TestPath.java @@ -170,4 +170,12 @@ public void startsWithShort() { public void startsWithShortComponent() { HAssert.assertFalse(Path.createEmpty().startsWith("a")); } + + @Test + public void subPath() { + HAssert.assertEquals(new Path<>("b"), new Path<>("a", "b", "c").subPath(1, -2)); + HAssert.assertEquals(new Path<>("b"), new Path<>("a", "b", "c").subPath(-3, 2)); + HAssert.assertEquals(new Path<>("a", "b", "c"), new Path<>("a", "b", "c").subPath(0, -1)); + HAssert.assertEquals(new Path<>("a", "b", "c"), new Path<>("a", "b", "c").subPath(-4, 3)); + } }