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 super O> 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 super B> type, final Function super A, ? extends B> function, final A... values) {
@SuppressWarnings("unchecked")
@@ -64,4 +64,53 @@ public static List map(final Function super A, ? extends B> 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));
+ }
}