Skip to content

Commit

Permalink
Merge pull request #382 from gdgib/G2-1652-TrieAPI
Browse files Browse the repository at this point in the history
G2-1652 Basic Trie API & Implementation
  • Loading branch information
gdgib authored Oct 5, 2024
2 parents dafade6 + 388c250 commit 2991565
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 8 deletions.
2 changes: 1 addition & 1 deletion ax-adt/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</dependency>
<dependency>
<groupId>com.g2forge.alexandria</groupId>
<artifactId>ax-collection</artifactId>
<artifactId>ax-path</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KT, V> {
public IOptional<V> get(IPath<KT> path);
}
32 changes: 30 additions & 2 deletions ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Node.java
Original file line number Diff line number Diff line change
@@ -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<KT, V> {
protected final IPath<KT> label;

protected final Map<KT, Node<KT, V>> children;

protected boolean isTerminal;

protected V value;

public Node(IPath<KT> label) {
this(label, new HashMap<>(), false, null);
}

public Node(IPath<KT> label, V value) {
this(label, new HashMap<>(), true, value);
}
}
48 changes: 48 additions & 0 deletions ax-adt/src/main/java/com/g2forge/alexandria/adt/trie/Trie.java
Original file line number Diff line number Diff line change
@@ -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<KT, V> implements ITrie<KT, V> {
protected Node<KT, V> root;

@Override
public IOptional<V> get(IPath<KT> path) {
final Node<KT, V> root = getRoot();
Node<KT, V> 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<KT, V> 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<KT> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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<Node<Character, String>> {
public interface IChildBuilder {
public NodeBuilder child(String label, String value);
}

public static IPath<Character> toLabel(String label) {
return new Path<>(HArray.toObject(label.toCharArray()));
}

protected final Node<Character, String> 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<Character, String> build() {
return node;
}

public NodeBuilder children(IConsumer1<NodeBuilder.IChildBuilder> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Character>()), new NodeBuilder("", null).build());;;
}

@Test
public void label() {
HAssert.assertEquals(new Node<>(new Path<Character>('a', 'b', 'c')), new NodeBuilder("abc", null).build());;;
}

@Test
public void child() {
final Node<Character, String> expected = new Node<>(new Path<Character>('a', 'b', 'c'));
expected.getChildren().put('d', new Node<>(new Path<Character>('d'), "value"));
HAssert.assertEquals(expected, new NodeBuilder("abc", null).children(c -> c.child("d", "value")).build());;;
}
}
Original file line number Diff line number Diff line change
@@ -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<Character, String> trie = new Trie<>(new NodeBuilder("t", null).children(c -> {
c.child("est", "test");
c.child("oast", "toast");
}).build());

@Test
public void roast() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("roast"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void temp() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("temp"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void test() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("test"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("test", result.get());
}

@Test
public void toast() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toast"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("toast", result.get());
}

@Test
public void toaster() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toaster"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void toasting() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toasting"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void toasti() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toasti"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void trip() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("trip"));
HAssert.assertFalse(result.isNotEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -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<Character, String> 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<String> result = trie.get(NodeBuilder.toLabel("roast"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void temp() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("temp"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void test() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("test"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("test", result.get());
}

@Test
public void toast() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toast"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("toast", result.get());
}

@Test
public void toaster() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toaster"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("toaster", result.get());
}

@Test
public void toasti() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toasti"));
HAssert.assertFalse(result.isNotEmpty());
}

@Test
public void toasting() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("toasting"));
HAssert.assertTrue(result.isNotEmpty());
HAssert.assertEquals("toasting", result.get());
}

@Test
public void trip() {
final IOptional<String> result = trie.get(NodeBuilder.toLabel("trip"));
HAssert.assertFalse(result.isNotEmpty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
@Helpers
@UtilityClass
public class HArray {
@SafeVarargs
public static <T> T[] create(T... array) {
return array;
}

@SuppressWarnings("unchecked")
public static <I extends O, O> O[] cast(Class<? super O> type, I... array) {
final Object retVal = Array.newInstance(type, array.length);
Expand Down Expand Up @@ -48,6 +43,11 @@ public static <T> boolean contains(T value, T... array) {
return false;
}

@SafeVarargs
public static <T> T[] create(T... array) {
return array;
}

@SafeVarargs
public static <A, B> B[] map(final Class<? super B> type, final Function<? super A, ? extends B> function, final A... values) {
@SuppressWarnings("unchecked")
Expand All @@ -64,4 +64,53 @@ public static <A, B> List<B> 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;
}
}
10 changes: 10 additions & 0 deletions ax-path/src/main/java/com/g2forge/alexandria/path/path/IPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public default T getLast() {

public IPath<T> 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<T> subPath(int fromIndex, int toIndex);

public default boolean isEmpty() {
return getComponents().isEmpty();
}
Expand Down
Loading

0 comments on commit 2991565

Please sign in to comment.