summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/esodata
diff options
context:
space:
mode:
authorBenjamin J. Culkin <bjculkin@mix.wvu.edu>2017-10-08 22:39:59 -0300
committerBenjamin J. Culkin <bjculkin@mix.wvu.edu>2017-10-08 22:39:59 -0300
commitc82e3b3b2de0633317ec8fc85925e91422820597 (patch)
tree96567416ce23c5ce85601f9cedc3a94bb1c55cba /base/src/main/java/bjc/utils/esodata
parentb3ac1c8690c3e14c879913e5dcc03a5f5e14876e (diff)
Start splitting into maven modules
Diffstat (limited to 'base/src/main/java/bjc/utils/esodata')
-rw-r--r--base/src/main/java/bjc/utils/esodata/AbbrevMap.java227
-rw-r--r--base/src/main/java/bjc/utils/esodata/Directory.java106
-rw-r--r--base/src/main/java/bjc/utils/esodata/DoubleTape.java258
-rw-r--r--base/src/main/java/bjc/utils/esodata/PushdownMap.java148
-rw-r--r--base/src/main/java/bjc/utils/esodata/QueueStack.java88
-rw-r--r--base/src/main/java/bjc/utils/esodata/SimpleDirectory.java95
-rw-r--r--base/src/main/java/bjc/utils/esodata/SimpleStack.java88
-rw-r--r--base/src/main/java/bjc/utils/esodata/SingleTape.java255
-rw-r--r--base/src/main/java/bjc/utils/esodata/SpaghettiStack.java99
-rw-r--r--base/src/main/java/bjc/utils/esodata/Stack.java459
-rw-r--r--base/src/main/java/bjc/utils/esodata/Tape.java126
-rw-r--r--base/src/main/java/bjc/utils/esodata/TapeChanger.java363
-rw-r--r--base/src/main/java/bjc/utils/esodata/TapeLibrary.java340
-rw-r--r--base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java105
14 files changed, 2757 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/esodata/AbbrevMap.java b/base/src/main/java/bjc/utils/esodata/AbbrevMap.java
new file mode 100644
index 0000000..0d54471
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/AbbrevMap.java
@@ -0,0 +1,227 @@
+package bjc.utils.esodata;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * Represents a mapping from a set of strings to a mapping of all unambiguous
+ * prefixes of their respective strings.
+ *
+ * This works the same as Ruby's Abbrev.
+ *
+ * @author EVE
+ *
+ */
+public class AbbrevMap {
+ /*
+ * All of the words we have abbreviations for.
+ */
+ private final Set<String> wrds;
+
+ /*
+ * Maps abbreviations to their strings.
+ */
+ private IMap<String, String> abbrevMap;
+
+ /*
+ * Counts how many times we've seen a substring.
+ */
+ private Set<String> seen;
+
+ /*
+ * Maps ambiguous abbreviations to the strings they could be.
+ */
+ private SetMultimap<String, String> ambMap;
+
+ /**
+ * Create a new abbreviation map.
+ *
+ * @param words
+ * The initial set of words to put in the map.
+ */
+ public AbbrevMap(final String... words) {
+ wrds = new HashSet<>(Arrays.asList(words));
+
+ recalculate();
+ }
+
+ /**
+ * Recalculate all the abbreviations in this map.
+ */
+ public void recalculate() {
+ abbrevMap = new FunctionalMap<>();
+
+ ambMap = HashMultimap.create();
+
+ seen = new HashSet<>();
+
+ for (final String word : wrds) {
+ /*
+ * A word always abbreviates to itself.
+ */
+ abbrevMap.put(word, word);
+
+ intAddWord(word);
+ }
+ }
+
+ /**
+ * Adds words to the abbreviation map.
+ *
+ * @param words
+ * The words to add to the abbreviation map.
+ */
+ public void addWords(final String... words) {
+ wrds.addAll(Arrays.asList(words));
+
+ for (final String word : words) {
+ /*
+ * A word always abbreviates to itself.
+ */
+ abbrevMap.put(word, word);
+
+ intAddWord(word);
+ }
+ }
+
+ /*
+ * Actually add abbreviations of a word.
+ */
+ private void intAddWord(final String word) {
+ /*
+ * Skip blank words.
+ */
+ if (word.equals("")) return;
+
+ /*
+ * Handle each possible abbreviation.
+ */
+ for (int i = word.length(); i > 0; i--) {
+ final String subword = word.substring(0, i);
+
+ if (seen.contains(subword)) {
+ /*
+ * Remove a mapping if its ambiguous and not a
+ * whole word.
+ */
+ if (abbrevMap.containsKey(subword) && !wrds.contains(subword)) {
+ final String oldword = abbrevMap.remove(subword);
+
+ ambMap.put(subword, oldword);
+ ambMap.put(subword, word);
+ } else if (!wrds.contains(subword)) {
+ ambMap.put(subword, word);
+ }
+ } else {
+ seen.add(subword);
+
+ abbrevMap.put(subword, word);
+ }
+ }
+ }
+
+ /**
+ * Removes words from the abbreviation map.
+ *
+ * NOTE: There may be inconsistent behavior after removing a word from
+ * the map. Use {@link AbbrevMap#recalculate()} to fix it if it occurs.
+ *
+ * @param words
+ * The words to remove.
+ */
+ public void removeWords(final String... words) {
+ wrds.removeAll(Arrays.asList(words));
+
+ for (final String word : words) {
+ intRemoveWord(word);
+ }
+ }
+
+ /*
+ * Actually remove a word.
+ */
+ private void intRemoveWord(final String word) {
+ /*
+ * Skip blank words.
+ */
+ if (word.equals("")) return;
+
+ /*
+ * Handle each possible abbreviation.
+ */
+ for (int i = word.length(); i > 0; i--) {
+ final String subword = word.substring(0, i);
+
+ if (abbrevMap.containsKey(subword)) {
+ abbrevMap.remove(subword);
+ } else {
+ ambMap.remove(subword, word);
+
+ final Set<String> possWords = ambMap.get(subword);
+
+ if (possWords.size() == 0) {
+ seen.remove(subword);
+ } else if (possWords.size() == 1) {
+ final String newWord = possWords.iterator().next();
+
+ abbrevMap.put(subword, newWord);
+ ambMap.remove(subword, newWord);
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert an abbreviation into all the strings it could abbreviate
+ * into.
+ *
+ * @param abbrev
+ * The abbreviation to convert.
+ *
+ * @return All the expansions for the provided abbreviation.
+ */
+ public String[] deabbrev(final String abbrev) {
+ if (abbrevMap.containsKey(abbrev))
+ return new String[] { abbrevMap.get(abbrev) };
+ else return ambMap.get(abbrev).toArray(new String[0]);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (wrds == null ? 0 : wrds.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof AbbrevMap)) return false;
+
+ final AbbrevMap other = (AbbrevMap) obj;
+
+ if (wrds == null) {
+ if (other.wrds != null) return false;
+ } else if (!wrds.equals(other.wrds)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final String fmt = "AbbrevMap [wrds=%s, abbrevMap=%s, seen=%s, ambMap=%s]";
+
+ return String.format(fmt, wrds, abbrevMap, seen, ambMap);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/Directory.java b/base/src/main/java/bjc/utils/esodata/Directory.java
new file mode 100644
index 0000000..17b70f5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/Directory.java
@@ -0,0 +1,106 @@
+package bjc.utils.esodata;
+
+/**
+ * Represents a hierarchical map.
+ *
+ * What's useful about this is that you can hand sub-directories to people and
+ * be able to ensure that they can't write outside of it.
+ *
+ * @param <K>
+ * The key type of the map.
+ * @param <V>
+ * The value type of the map.
+ */
+public interface Directory<K, V> {
+ /**
+ * Retrieves a given sub-directory.
+ *
+ * @param key
+ * The key to retrieve the sub-directory for.
+ *
+ * @return The sub-directory under that name.
+ *
+ * @throws IllegalArgumentException
+ * If the given sub-directory doesn't exist.
+ */
+ Directory<K, V> getSubdirectory(K key);
+
+ /**
+ * Check if a given sub-directory exists.
+ *
+ * @param key
+ * The key to look for the sub-directory under.
+ *
+ * @return Whether or not a sub-directory of that name exists.
+ */
+ boolean hasSubdirectory(K key);
+
+ /**
+ * Insert a sub-directory into the dictionary.
+ *
+ * @param key
+ * The name of the new sub-directory
+ * @param value
+ * The sub-directory to insert
+ *
+ * @return The old sub-directory attached to this key, or null if such a
+ * sub-directory didn't exist
+ */
+ Directory<K, V> putSubdirectory(K key, Directory<K, V> value);
+
+ /**
+ * Create a new sub-directory.
+ *
+ * Will fail if a sub-directory of that name already exists.
+ *
+ * @param key
+ * The name of the new sub-directory.
+ *
+ * @return The new sub-directory, or null if one by that name already
+ * exists.
+ */
+ default Directory<K, V> newSubdirectory(final K key) {
+ if (hasSubdirectory(key)) return null;
+
+ final Directory<K, V> dir = new SimpleDirectory<>();
+
+ putSubdirectory(key, dir);
+
+ return dir;
+ }
+
+ /**
+ * Check if the directory contains a data-item under the given key.
+ *
+ * @param key
+ * The key to check for.
+ *
+ * @return Whether or not there is a data item for the given key.
+ */
+ boolean containsKey(K key);
+
+ /**
+ * Retrieve a given data-item from the directory.
+ *
+ * @param key
+ * The key to retrieve data for.
+ *
+ * @return The value for the given key.
+ *
+ * @throws IllegalArgumentException
+ * If no value exists for the given key.
+ */
+ V getKey(K key);
+
+ /**
+ * Insert a data-item into the directory.
+ *
+ * @param key
+ * The key to insert into.
+ * @param val
+ * The value to insert.
+ *
+ * @return The old value of key, or null if such a value didn't exist.
+ */
+ V putKey(K key, V val);
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/esodata/DoubleTape.java b/base/src/main/java/bjc/utils/esodata/DoubleTape.java
new file mode 100644
index 0000000..5c463c6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/DoubleTape.java
@@ -0,0 +1,258 @@
+package bjc.utils.esodata;
+
+/**
+ * Double-sided tape is essentially two tapes stuck together with a shared
+ * cursor.
+ *
+ * The main way a double-sided tape differs is that it can be flipped, allowing
+ * access to another set of data.
+ *
+ * However, there is only one cursor, and the position of the cursor on one side
+ * is the inverse of the position on the other side.
+ *
+ * When one side is extended, a null will be inserted into the inactive side
+ * regardless of the auto-extension policy of the tape. The policy will still be
+ * respected for the active side.
+ *
+ * All operations that refer to the tape refer to the currently active side of
+ * the tape, except for flip.
+ *
+ * Flip refers to the entire tape for 'obvious' reasons.
+ *
+ * @param <T>
+ * The element type of the tape.
+ * @author bjculkin
+ */
+public class DoubleTape<T> implements Tape<T> {
+ private Tape<T> front;
+ private Tape<T> back;
+
+ /**
+ * Create a new empty double-sided tape that doesn't autoextend.
+ */
+ public DoubleTape() {
+ this(false);
+ }
+
+ /**
+ * Create a new empty double-sided tape that follows the specified
+ * auto-extension policy.
+ *
+ * @param autoExtnd
+ * Whether or not to auto-extend the tape to the right w/
+ * nulls.
+ */
+ public DoubleTape(final boolean autoExtnd) {
+ front = new SingleTape<>(autoExtnd);
+ back = new SingleTape<>(autoExtnd);
+ }
+
+ /**
+ * Get the item the tape is currently on.
+ *
+ * @return The item the tape is on.
+ */
+ @Override
+ public T item() {
+ return front.item();
+ }
+
+ /**
+ * Set the item the tape is currently on.
+ *
+ * @param itm
+ * The new value for the tape item.
+ */
+ @Override
+ public void item(final T itm) {
+ front.item(itm);
+ }
+
+ /**
+ * Get the current number of elements in the tape.
+ *
+ * @return The current number of elements in the tape.
+ */
+ @Override
+ public int size() {
+ return front.size();
+ }
+
+ @Override
+ public int position() {
+ return front.position();
+ }
+
+ /**
+ * Insert an element before the current item.
+ *
+ * @param itm
+ * The item to add.
+ */
+ @Override
+ public void insertBefore(final T itm) {
+ front.insertBefore(itm);
+ back.insertAfter(null);
+ }
+
+ /**
+ * Insert an element after the current item.
+ */
+ @Override
+ public void insertAfter(final T itm) {
+ front.insertAfter(itm);
+ back.insertBefore(itm);
+ }
+
+ /**
+ * Remove the current element.
+ *
+ * Also moves the cursor back one step if possible to maintain relative
+ * position, and removes the corresponding item from the non-active side
+ *
+ * @return The removed item from the active side.
+ */
+ @Override
+ public T remove() {
+ back.remove();
+
+ return front.remove();
+ }
+
+ /**
+ * Move the cursor to the left-most position.
+ */
+ @Override
+ public void first() {
+ front.first();
+ back.last();
+ }
+
+ /**
+ * Move the cursor the right-most position.
+ */
+ @Override
+ public void last() {
+ front.last();
+ back.first();
+ }
+
+ /**
+ * Move the cursor one space left.
+ *
+ * The cursor can't go past zero.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left() {
+ return left(1);
+ }
+
+ /**
+ * Move the cursor the specified amount left.
+ *
+ * The cursor can't go past zero. Attempts to move the cursor by amounts
+ * that would exceed zero don't move the cursor at all.
+ *
+ * @param amt
+ * The amount to attempt to move the cursor left.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left(final int amt) {
+ final boolean succ = front.left(amt);
+
+ if (succ) {
+ back.right(amt);
+ }
+
+ return succ;
+ }
+
+ /**
+ * Move the cursor one space right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right() {
+ return right(1);
+ }
+
+ /**
+ * Move the cursor the specified amount right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @param amt
+ * The amount to move the cursor right by.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right(final int amt) {
+ final boolean succ = front.right(amt);
+
+ if (succ) {
+ back.left(amt);
+ }
+
+ return succ;
+ }
+
+ /**
+ * Flips the tape.
+ *
+ * The active side becomes inactive, and the inactive side becomes
+ * active.
+ */
+ public void flip() {
+ final Tape<T> tmp = front;
+
+ front = back;
+
+ back = tmp;
+ }
+
+ @Override
+ public boolean isDoubleSided() {
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (back == null ? 0 : back.hashCode());
+ result = prime * result + (front == null ? 0 : front.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof DoubleTape<?>)) return false;
+
+ final DoubleTape<?> other = (DoubleTape<?>) obj;
+
+ if (back == null) {
+ if (other.back != null) return false;
+ } else if (!back.equals(other.back)) return false;
+
+ if (front == null) {
+ if (other.front != null) return false;
+ } else if (!front.equals(other.front)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("DoubleTape [front=%s, back=%s]", front, back);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/PushdownMap.java b/base/src/main/java/bjc/utils/esodata/PushdownMap.java
new file mode 100644
index 0000000..a631704
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/PushdownMap.java
@@ -0,0 +1,148 @@
+package bjc.utils.esodata;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A variant of a map where inserting a duplicate key shadows the existing value
+ * instead of replacing it.
+ *
+ * @author EVE
+ *
+ * @param <KeyType>
+ * The key of the map.
+ * @param <ValueType>
+ * The values in the map.
+ */
+public class PushdownMap<KeyType, ValueType> implements IMap<KeyType, ValueType> {
+ private final IMap<KeyType, Stack<ValueType>> backing;
+
+ /**
+ * Create a new empty stack-based map.
+ */
+ public PushdownMap() {
+ backing = new FunctionalMap<>();
+ }
+
+ private PushdownMap(final IMap<KeyType, Stack<ValueType>> back) {
+ backing = back;
+ }
+
+ @Override
+ public void clear() {
+ backing.clear();
+ }
+
+ @Override
+ public boolean containsKey(final KeyType key) {
+ return backing.containsKey(key);
+ }
+
+ @Override
+ public IMap<KeyType, ValueType> extend() {
+ return new PushdownMap<>(backing.extend());
+ }
+
+ @Override
+ public void forEach(final BiConsumer<KeyType, ValueType> action) {
+ backing.forEach((key, stk) -> action.accept(key, stk.top()));
+ }
+
+ @Override
+ public void forEachKey(final Consumer<KeyType> action) {
+ backing.forEachKey(action);
+ }
+
+ @Override
+ public void forEachValue(final Consumer<ValueType> action) {
+ backing.forEachValue(stk -> action.accept(stk.top()));
+ }
+
+ @Override
+ public ValueType get(final KeyType key) {
+ return backing.get(key).top();
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public IList<KeyType> keyList() {
+ return backing.keyList();
+ }
+
+ @Override
+ public <V2> IMap<KeyType, V2> transform(final Function<ValueType, V2> transformer) {
+ throw new UnsupportedOperationException("Cannot transform pushdown maps.");
+ }
+
+ @Override
+ public ValueType put(final KeyType key, final ValueType val) {
+ if (backing.containsKey(key)) {
+ final Stack<ValueType> stk = backing.get(key);
+
+ final ValueType vl = stk.top();
+
+ stk.push(val);
+
+ return vl;
+ } else {
+ final Stack<ValueType> stk = new SimpleStack<>();
+
+ stk.push(val);
+
+ return null;
+ }
+ }
+
+ @Override
+ public ValueType remove(final KeyType key) {
+ final Stack<ValueType> stk = backing.get(key);
+
+ if (stk.size() > 1)
+ return stk.pop();
+ else return backing.remove(key).top();
+ }
+
+ @Override
+ public IList<ValueType> valueList() {
+ return backing.valueList().map(stk -> stk.top());
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (backing == null ? 0 : backing.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof PushdownMap<?, ?>)) return false;
+
+ final PushdownMap<?, ?> other = (PushdownMap<?, ?>) obj;
+
+ if (backing == null) {
+ if (other.backing != null) return false;
+ } else if (!backing.equals(other.backing)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PushdownMap [backing=%s]", backing);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/QueueStack.java b/base/src/main/java/bjc/utils/esodata/QueueStack.java
new file mode 100644
index 0000000..850598a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/QueueStack.java
@@ -0,0 +1,88 @@
+package bjc.utils.esodata;
+
+import java.util.Deque;
+import java.util.LinkedList;
+
+/**
+ * A FIFO implementation of a stack.
+ *
+ * @param <T>
+ * The datatype stored in the stack.
+ * @author Ben Culkin
+ */
+public class QueueStack<T> extends Stack<T> {
+ private final Deque<T> backing;
+
+ /**
+ * Create a new empty stack queue.
+ *
+ */
+ public QueueStack() {
+ backing = new LinkedList<>();
+ }
+
+ @Override
+ public void push(final T elm) {
+ backing.add(elm);
+ }
+
+ @Override
+ public T pop() {
+ if (backing.isEmpty()) throw new StackUnderflowException();
+
+ return backing.remove();
+ }
+
+ @Override
+ public T top() {
+ if (backing.isEmpty()) throw new StackUnderflowException();
+
+ return backing.peek();
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public boolean empty() {
+ return backing.size() == 0;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T[] toArray() {
+ return (T[]) backing.toArray();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("QueueStack [backing=%s]", backing);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (backing == null ? 0 : backing.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof QueueStack<?>)) return false;
+
+ final QueueStack<?> other = (QueueStack<?>) obj;
+
+ if (backing == null) {
+ if (other.backing != null) return false;
+ } else if (!backing.equals(other.backing)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java b/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java
new file mode 100644
index 0000000..69fd019
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/SimpleDirectory.java
@@ -0,0 +1,95 @@
+package bjc.utils.esodata;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * Simple implementation of {@link Directory}.
+ *
+ * Has a split namespace for data and children.
+ *
+ * @author EVE
+ *
+ * @param <K>
+ * The key type of the directory.
+ * @param <V>
+ * The value type of the directory.
+ */
+public class SimpleDirectory<K, V> implements Directory<K, V> {
+ private final IMap<K, Directory<K, V>> children;
+
+ private final IMap<K, V> data;
+
+ /**
+ * Create a new directory.
+ */
+ public SimpleDirectory() {
+ children = new FunctionalMap<>();
+ data = new FunctionalMap<>();
+ }
+
+ @Override
+ public Directory<K, V> getSubdirectory(final K key) {
+ return children.get(key);
+ }
+
+ @Override
+ public boolean hasSubdirectory(final K key) {
+ return children.containsKey(key);
+ }
+
+ @Override
+ public Directory<K, V> putSubdirectory(final K key, final Directory<K, V> val) {
+ return children.put(key, val);
+ }
+
+ @Override
+ public boolean containsKey(final K key) {
+ return data.containsKey(key);
+ }
+
+ @Override
+ public V getKey(final K key) {
+ return data.get(key);
+ }
+
+ @Override
+ public V putKey(final K key, final V val) {
+ return data.put(key, val);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (children == null ? 0 : children.hashCode());
+ result = prime * result + (data == null ? 0 : data.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof SimpleDirectory<?, ?>)) return false;
+
+ final SimpleDirectory<?, ?> other = (SimpleDirectory<?, ?>) obj;
+
+ if (children == null) {
+ if (other.children != null) return false;
+ } else if (!children.equals(other.children)) return false;
+
+ if (data == null) {
+ if (other.data != null) return false;
+ } else if (!data.equals(other.data)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SimpleDirectory [children=%s, data=%s]", children, data);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/esodata/SimpleStack.java b/base/src/main/java/bjc/utils/esodata/SimpleStack.java
new file mode 100644
index 0000000..fdb3300
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/SimpleStack.java
@@ -0,0 +1,88 @@
+package bjc.utils.esodata;
+
+import java.util.Deque;
+import java.util.LinkedList;
+
+/**
+ * Simple implementation of a stack.
+ *
+ * @param <T>
+ * The datatype stored in the stack.
+ * @author Ben Culkin
+ */
+public class SimpleStack<T> extends Stack<T> {
+ private final Deque<T> backing;
+
+ /**
+ * Create a new empty stack.
+ *
+ */
+ public SimpleStack() {
+ backing = new LinkedList<>();
+ }
+
+ @Override
+ public void push(final T elm) {
+ backing.push(elm);
+ }
+
+ @Override
+ public T pop() {
+ if (backing.isEmpty()) throw new StackUnderflowException();
+
+ return backing.pop();
+ }
+
+ @Override
+ public T top() {
+ if (backing.isEmpty()) throw new StackUnderflowException();
+
+ return backing.peek();
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public boolean empty() {
+ return backing.size() == 0;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T[] toArray() {
+ return (T[]) backing.toArray();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (backing == null ? 0 : backing.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof SimpleStack<?>)) return false;
+
+ final SimpleStack<?> other = (SimpleStack<?>) obj;
+
+ if (backing == null) {
+ if (other.backing != null) return false;
+ } else if (!backing.equals(other.backing)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SimpleStack [backing=%s]", backing);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/SingleTape.java b/base/src/main/java/bjc/utils/esodata/SingleTape.java
new file mode 100644
index 0000000..c50be92
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/SingleTape.java
@@ -0,0 +1,255 @@
+package bjc.utils.esodata;
+
+import java.util.ArrayList;
+
+/**
+ * A tape is a one-dimensional array that can only be accessed in one position
+ * at a time.
+ *
+ * A tape is essentially a 1D array with a cursor attached to it, and you can
+ * only affect elements at that cursor. The size of the array is theoretically
+ * unbounded to the right, but in practice bounded by available memory.
+ *
+ * You can choose whether or not you want the tape to automatically extend
+ * itself to the right with null elements by specifying its auto-extension
+ * policy.
+ *
+ * @param <T>
+ * The element type of the tape.
+ *
+ * @author bjculkin
+ */
+public class SingleTape<T> implements Tape<T> {
+ protected ArrayList<T> backing;
+ protected int pos;
+
+ protected boolean autoExtend;
+
+ /**
+ * Create a new tape with the specified contents that doesn't
+ * autoextend.
+ */
+ public SingleTape(T... vals) {
+ autoExtend = false;
+
+ backing = new ArrayList(vals.length);
+
+ for(T val : vals) {
+ backing.add(val);
+ }
+ }
+ /**
+ * Create a new empty tape that doesn't autoextend.
+ */
+ public SingleTape() {
+ this(false);
+ }
+
+ /**
+ * Create a new empty tape that follows the specified auto-extension
+ * policy.
+ *
+ * @param autoExtnd
+ * Whether or not to auto-extend the tape to the right w/
+ * nulls.
+ */
+ public SingleTape(final boolean autoExtnd) {
+ autoExtend = autoExtnd;
+
+ backing = new ArrayList<>();
+ }
+
+ /**
+ * Get the item the tape is currently on.
+ *
+ * @return The item the tape is on.
+ */
+ @Override
+ public T item() {
+ return backing.get(pos);
+ }
+
+ /**
+ * Set the item the tape is currently on.
+ *
+ * @param itm
+ * The new value for the tape item.
+ */
+ @Override
+ public void item(final T itm) {
+ backing.set(pos, itm);
+ }
+
+ /**
+ * Get the current number of elements in the tape.
+ *
+ * @return The current number of elements in the tape.
+ */
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public int position() {
+ return pos;
+ }
+
+ /**
+ * Insert an element before the current item.
+ *
+ * @param itm
+ * The item to add.
+ */
+ @Override
+ public void insertBefore(final T itm) {
+ backing.add(pos, itm);
+ }
+
+ /**
+ * Insert an element after the current item.
+ */
+ @Override
+ public void insertAfter(final T itm) {
+ if (pos == backing.size() - 1) {
+ backing.add(itm);
+ } else {
+ backing.add(pos + 1, itm);
+ }
+ }
+
+ /**
+ * Remove the current element.
+ *
+ * Also moves the cursor back one step if possible to maintain relative
+ * position.
+ *
+ * @return The removed item.
+ */
+ @Override
+ public T remove() {
+ final T res = backing.remove(pos);
+ if (pos != 0) {
+ pos -= 1;
+ }
+ return res;
+ }
+
+ /**
+ * Move the cursor to the left-most position.
+ */
+ @Override
+ public void first() {
+ pos = 0;
+ }
+
+ /**
+ * Move the cursor the right-most position.
+ */
+ @Override
+ public void last() {
+ pos = backing.size() - 1;
+ }
+
+ /**
+ * Move the cursor one space left.
+ *
+ * The cursor can't go past zero.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left() {
+ return left(1);
+ }
+
+ /**
+ * Move the cursor the specified amount left.
+ *
+ * The cursor can't go past zero. Attempts to move the cursor by amounts
+ * that would exceed zero don't move the cursor at all.
+ *
+ * @param amt
+ * The amount to attempt to move the cursor left.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left(final int amt) {
+ if (pos - amt < 0) return false;
+
+ pos -= amt;
+ return true;
+ }
+
+ /**
+ * Move the cursor one space right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right() {
+ return right(1);
+ }
+
+ /**
+ * Move the cursor the specified amount right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @param amt
+ * The amount to move the cursor right by.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right(final int amt) {
+ if (pos + amt >= backing.size() - 1) {
+ if (autoExtend) {
+ while (pos + amt >= backing.size() - 1) {
+ backing.add(null);
+ }
+ } else return false;
+ }
+
+ pos += amt;
+ return true;
+ }
+
+ @Override
+ public boolean isDoubleSided() {
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (backing == null ? 0 : backing.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof SingleTape<?>)) return false;
+
+ final SingleTape<?> other = (SingleTape<?>) obj;
+
+ if (backing == null) {
+ if (other.backing != null) return false;
+ } else if (!backing.equals(other.backing)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SingleTape [backing=%s, pos=%s, autoExtend=%s]", backing, pos, autoExtend);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java b/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java
new file mode 100644
index 0000000..7c8c757
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/SpaghettiStack.java
@@ -0,0 +1,99 @@
+package bjc.utils.esodata;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+/*
+ * Implements a spaghetti stack, which is a stack that is branched off of a
+ * parent stack.
+ *
+ * @param T The datatype stored in the stack.
+ * @author Ben Culkin
+ */
+class SpaghettiStack<T> extends Stack<T> {
+ private final Stack<T> backing;
+
+ private final Stack<T> parent;
+
+ /**
+ * Create a new empty spaghetti stack, off of the specified parent.
+ *
+ * @param par
+ * The parent stack
+ */
+ public SpaghettiStack(final Stack<T> par) {
+ backing = new SimpleStack<>();
+
+ parent = par;
+ }
+
+ @Override
+ public void push(final T elm) {
+ backing.push(elm);
+ }
+
+ @Override
+ public T pop() {
+ if (backing.empty()) return parent.pop();
+
+ return backing.pop();
+ }
+
+ @Override
+ public T top() {
+ if (backing.empty()) return parent.top();
+
+ return backing.top();
+ }
+
+ @Override
+ public int size() {
+ return parent.size() + backing.size();
+ }
+
+ @Override
+ public boolean empty() {
+ return backing.empty() && parent.empty();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T[] toArray() {
+ return (T[]) Stream.concat(Arrays.stream(parent.toArray()), Arrays.stream(backing.toArray())).toArray();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (backing == null ? 0 : backing.hashCode());
+ result = prime * result + (parent == null ? 0 : parent.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof SpaghettiStack<?>)) return false;
+
+ final SpaghettiStack<?> other = (SpaghettiStack<?>) obj;
+
+ if (backing == null) {
+ if (other.backing != null) return false;
+ } else if (!backing.equals(other.backing)) return false;
+
+ if (parent == null) {
+ if (other.parent != null) return false;
+ } else if (!parent.equals(other.parent)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SpaghettiStack [backing=%s, parent=%s]", backing, parent);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/Stack.java b/base/src/main/java/bjc/utils/esodata/Stack.java
new file mode 100644
index 0000000..9d74e9a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/Stack.java
@@ -0,0 +1,459 @@
+package bjc.utils.esodata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A stack, with support for combinators.
+ *
+ * A FILO stack with support for forth/factor style combinators.
+ *
+ * <p>
+ * <h2>Stack underflow</h2>
+ * <p>
+ * NOTE: In general, using any operation that attempts to remove more data from
+ * the stack than exists will cause a {@link StackUnderflowException} to be
+ * thrown. Check the size of the stack if you want to avoid this.
+ * <p>
+ * </p>
+ *
+ * @param <T>
+ * The datatype stored in the stack.
+ *
+ * @author Ben Culkin
+ */
+public abstract class Stack<T> {
+ /**
+ * The exception thrown when attempting to access an element from the
+ * stack that isn't there.
+ *
+ * @author EVE
+ *
+ */
+ public static class StackUnderflowException extends RuntimeException {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1423867176204571539L;
+ }
+
+ /**
+ * Push an element onto the stack.
+ *
+ * @param elm
+ * The element to insert.
+ */
+ public abstract void push(T elm);
+
+ /**
+ * Pop an element off of the stack.
+ *
+ * @return The element on top of the stack.
+ */
+ public abstract T pop();
+
+ /**
+ * Retrieve the top element of this stack without removing it from the
+ * stack.
+ *
+ * @return The top element of this stack.
+ */
+ public abstract T top();
+
+ /**
+ * Get the number of elements in the stack.
+ *
+ * @return the number of elements in the stack.
+ */
+ public abstract int size();
+
+ /**
+ * Check if the stack is empty.
+ *
+ * @return Whether or not the stack is empty.
+ */
+ public abstract boolean empty();
+
+ /**
+ * Create a spaghetti stack branching off of this one.
+ *
+ * @return A spaghetti stack with this stack as a parent.
+ */
+ public Stack<T> spaghettify() {
+ return new SpaghettiStack<>(this);
+ }
+
+ /*
+ * Basic combinators
+ */
+
+ /**
+ * Drop n items from the stack.
+ *
+ * @param n
+ * The number of items to drop.
+ */
+ public void drop(final int n) {
+ for (int i = 0; i < n; i++) {
+ pop();
+ }
+ }
+
+ /**
+ * Drop one item from the stack.
+ */
+ public void drop() {
+ drop(1);
+ }
+
+ /**
+ * Delete n items below the current one.
+ *
+ * @param n
+ * The number of items below the top to delete.
+ */
+ public void nip(final int n) {
+ final T elm = pop();
+
+ drop(n);
+
+ push(elm);
+ }
+
+ /**
+ * Delete the second element in the stack.
+ */
+ public void nip() {
+ nip(1);
+ }
+
+ /**
+ * Replicate the top n items of the stack m times.
+ *
+ * @param n
+ * The number of items to duplicate.
+ * @param m
+ * The number of times to duplicate items.
+ */
+ public void multidup(final int n, final int m) {
+ final List<T> lst = new ArrayList<>(n);
+
+ for (int i = n; i > 0; i--) {
+ lst.set(i - 1, pop());
+ }
+
+ for (int i = 0; i < m; i++) {
+ for (final T elm : lst) {
+ push(elm);
+ }
+ }
+ }
+
+ /**
+ * Duplicate the top n items of the stack.
+ *
+ * @param n
+ * The number of items to duplicate.
+ */
+ public void dup(final int n) {
+ multidup(n, 2);
+ }
+
+ /**
+ * Duplicate the top item on the stack.
+ */
+ public void dup() {
+ dup(1);
+ }
+
+ /**
+ * Replicate the n elements below the top one m times.
+ *
+ * @param n
+ * The number of items to duplicate.
+ * @param m
+ * The number of times to duplicate items.
+ */
+ public void multiover(final int n, final int m) {
+ final List<T> lst = new ArrayList<>(n);
+
+ final T elm = pop();
+
+ for (int i = n; i > 0; i--) {
+ lst.set(i - 1, pop());
+ }
+
+ for (final T nelm : lst) {
+ push(nelm);
+ }
+ push(elm);
+
+ for (int i = 1; i < m; i++) {
+ for (final T nelm : lst) {
+ push(nelm);
+ }
+ }
+ }
+
+ /**
+ * Duplicate the n elements below the top one.
+ *
+ * @param n
+ * The number of items to duplicate.
+ */
+ public void over(final int n) {
+ multiover(n, 2);
+ }
+
+ /**
+ * Duplicate the second item in the stack.
+ */
+ public void over() {
+ over(1);
+ }
+
+ /**
+ * Duplicate the third item in the stack.
+ */
+ public void pick() {
+ final T z = pop();
+ final T y = pop();
+ final T x = pop();
+
+ push(x);
+ push(y);
+ push(z);
+ push(x);
+ }
+
+ /**
+ * Swap the top two items on the stack.
+ */
+ public void swap() {
+ final T y = pop();
+ final T x = pop();
+
+ push(y);
+ push(x);
+ }
+
+ /**
+ * Duplicate the second item below the first item.
+ */
+ public void deepdup() {
+ final T y = pop();
+ final T x = pop();
+
+ push(x);
+ push(x);
+ push(y);
+ }
+
+ /**
+ * Swap the second and third items in the stack.
+ */
+ public void deepswap() {
+ final T z = pop();
+ final T y = pop();
+ final T x = pop();
+
+ push(y);
+ push(x);
+ push(z);
+ }
+
+ /**
+ * Rotate the top three items on the stack
+ */
+ public void rot() {
+ final T z = pop();
+ final T y = pop();
+ final T x = pop();
+
+ push(y);
+ push(z);
+ push(x);
+ }
+
+ /**
+ * Inversely rotate the top three items on the stack
+ */
+ public void invrot() {
+ final T z = pop();
+ final T y = pop();
+ final T x = pop();
+
+ push(z);
+ push(x);
+ push(y);
+ }
+
+ /*
+ * Dataflow Combinators
+ */
+ /**
+ * Hides the top n elements on the stack from cons.
+ *
+ * @param n
+ * The number of elements to hide.
+ * @param cons
+ * The action to hide the elements from
+ */
+ public void dip(final int n, final Consumer<Stack<T>> cons) {
+ final List<T> elms = new ArrayList<>(n);
+
+ for (int i = n; i > 0; i--) {
+ elms.set(i - 1, pop());
+ }
+
+ cons.accept(this);
+
+ for (final T elm : elms) {
+ push(elm);
+ }
+ }
+
+ /**
+ * Hide the top element of the stack from cons.
+ *
+ * @param cons
+ * The action to hide the top from
+ */
+ public void dip(final Consumer<Stack<T>> cons) {
+ dip(1, cons);
+ }
+
+ /**
+ * Copy the top n elements on the stack, replacing them once cons is
+ * done.
+ *
+ * @param n
+ * The number of elements to copy.
+ * @param cons
+ * The action to execute.
+ */
+ public void keep(final int n, final Consumer<Stack<T>> cons) {
+ dup(n);
+ dip(n, cons);
+ }
+
+ /**
+ * Apply all the actions in conses to the top n elements of the stack.
+ *
+ * @param n
+ * The number of elements to give to cons.
+ * @param conses
+ * The actions to execute.
+ */
+ public void multicleave(final int n, final List<Consumer<Stack<T>>> conses) {
+ final List<T> elms = new ArrayList<>(n);
+
+ for (int i = n; i > 0; i--) {
+ elms.set(i - 1, pop());
+ }
+
+ for (final Consumer<Stack<T>> cons : conses) {
+ for (final T elm : elms) {
+ push(elm);
+ }
+
+ cons.accept(this);
+ }
+ }
+
+ /**
+ * Apply all the actions in conses to the top element of the stack.
+ *
+ * @param conses
+ * The actions to execute.
+ */
+ public void cleave(final List<Consumer<Stack<T>>> conses) {
+ multicleave(1, conses);
+ }
+
+ /**
+ * Apply every action in cons to n arguments.
+ *
+ * @param n
+ * The number of parameters each action takes.
+ * @param conses
+ * The actions to execute.
+ */
+ public void multispread(final int n, final List<Consumer<Stack<T>>> conses) {
+ final List<List<T>> nelms = new ArrayList<>(conses.size());
+
+ for (int i = conses.size(); i > 0; i--) {
+ final List<T> elms = new ArrayList<>(n);
+
+ for (int j = n; j > 0; j--) {
+ elms.set(j, pop());
+ }
+
+ nelms.set(i, elms);
+ }
+
+ int i = 0;
+ for (final List<T> elms : nelms) {
+ for (final T elm : elms) {
+ push(elm);
+ }
+
+ conses.get(i).accept(this);
+ i += 1;
+ }
+ }
+
+ /**
+ * Apply the actions in cons to corresponding elements from the stack.
+ *
+ * @param conses
+ * The actions to execute.
+ */
+ public void spread(final List<Consumer<Stack<T>>> conses) {
+ multispread(1, conses);
+ }
+
+ /**
+ * Apply the action in cons to the first m groups of n arguments.
+ *
+ * @param n
+ * The number of arguments cons takes.
+ * @param m
+ * The number of time to call cons.
+ * @param cons
+ * The action to execute.
+ */
+ public void multiapply(final int n, final int m, final Consumer<Stack<T>> cons) {
+ final List<Consumer<Stack<T>>> conses = new ArrayList<>(m);
+
+ for (int i = 0; i < m; i++) {
+ conses.add(cons);
+ }
+
+ multispread(n, conses);
+ }
+
+ /**
+ * Apply cons n times to the corresponding elements in the stack.
+ *
+ * @param n
+ * The number of times to execute cons.
+ * @param cons
+ * The action to execute.
+ */
+ public void apply(final int n, final Consumer<Stack<T>> cons) {
+ multiapply(1, n, cons);
+ }
+
+ /*
+ * Misc. functions
+ */
+ /**
+ * Get an array representing this stack.
+ *
+ * @return The stack as an array.
+ */
+ public abstract T[] toArray();
+}
diff --git a/base/src/main/java/bjc/utils/esodata/Tape.java b/base/src/main/java/bjc/utils/esodata/Tape.java
new file mode 100644
index 0000000..b6a2c01
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/Tape.java
@@ -0,0 +1,126 @@
+package bjc.utils.esodata;
+
+/**
+ * Interface for something that acts like a tape.
+ *
+ * A tape is essentially a 1D array with a cursor attached to it, and you can
+ * only affect elements at that cursor. The size of the array is theoretically
+ * unbounded to the right, but in practice bounded by available memory.
+ *
+ * @param <T>
+ * The element type of the tape.
+ *
+ * @author bjculkin
+ */
+public interface Tape<T> {
+ /**
+ * Get the item the tape is currently on.
+ *
+ * @return The item the tape is on.
+ */
+ T item();
+
+ /**
+ * Set the item the tape is currently on.
+ *
+ * @param itm
+ * The new value for the tape item.
+ */
+ void item(T itm);
+
+ /**
+ * Get the current number of elements in the tape.
+ *
+ * @return The current number of elements in the tape.
+ */
+ int size();
+
+ /**
+ * Get the position of the current item.
+ *
+ * @return The position of the current item.
+ */
+ int position();
+
+ /**
+ * Insert an element before the current item.
+ *
+ * @param itm
+ * The item to add.
+ */
+ void insertBefore(T itm);
+
+ /**
+ * Insert an element after the current item.
+ *
+ * @param itm
+ * The item to insert.
+ */
+ void insertAfter(T itm);
+
+ /**
+ * Remove the current element.
+ *
+ * Also moves the cursor back one step if possible to maintain relative
+ * position.
+ *
+ * @return The removed item.
+ */
+ T remove();
+
+ /**
+ * Move the cursor to the left-most position.
+ */
+ void first();
+
+ /**
+ * Move the cursor the right-most position.
+ */
+ void last();
+
+ /**
+ * Move the cursor one space left.
+ *
+ * The cursor can't go past zero.
+ *
+ * @return True if the cursor was moved left.
+ */
+ boolean left();
+
+ /**
+ * Move the cursor the specified amount left.
+ *
+ * The cursor can't go past zero. Attempts to move the cursor by amounts
+ * that would exceed zero don't move the cursor at all.
+ *
+ * @param amt
+ * The amount to attempt to move the cursor left.
+ *
+ * @return True if the cursor was moved left.
+ */
+ boolean left(int amt);
+
+ /**
+ * Move the cursor one space right.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ boolean right();
+
+ /**
+ * Move the cursor the specified amount right.
+ *
+ * @param amt
+ * The amount to move the cursor right by.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ boolean right(int amt);
+
+ /**
+ * Is this tape double sided?
+ *
+ * @return Whether or not this tape is double-sided.
+ */
+ boolean isDoubleSided();
+}
diff --git a/base/src/main/java/bjc/utils/esodata/TapeChanger.java b/base/src/main/java/bjc/utils/esodata/TapeChanger.java
new file mode 100644
index 0000000..dc885bc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/TapeChanger.java
@@ -0,0 +1,363 @@
+package bjc.utils.esodata;
+
+/**
+ * A tape changer is essentially a tape of tapes.
+ *
+ * It has a current tape that you can do operations to, but also operations to
+ * add/remove other tapes.
+ *
+ * If there is no tape currently loaded into the changer, all the methods will
+ * either return null/false.
+ *
+ * @param <T>
+ * The element type of the tapes.
+ */
+public class TapeChanger<T> implements Tape<T> {
+ private Tape<Tape<T>> tapes;
+ private Tape<T> currentTape;
+
+ /**
+ * Create a new empty tape changer.
+ */
+ public TapeChanger() {
+ tapes = new SingleTape<>();
+ }
+
+ /**
+ * Create a new tape changer with the specified tapes.
+ *
+ * @param current
+ * The tape to mount first.
+ * @param others
+ * The tapes to put in this tape changer.
+ */
+ @SafeVarargs
+ public TapeChanger(final Tape<T> current, final Tape<T>... others) {
+ this();
+
+ tapes.insertBefore(current);
+
+ for (final Tape<T> tp : others) {
+ tapes.insertAfter(tp);
+ tapes.right();
+ }
+
+ tapes.first();
+ currentTape = tapes.item();
+ }
+
+ /**
+ * Get the item the tape is currently on.
+ *
+ * @return The item the tape is on.
+ */
+ @Override
+ public T item() {
+ if (currentTape == null) return null;
+
+ return currentTape.item();
+ }
+
+ /**
+ * Set the item the tape is currently on.
+ *
+ * @param itm
+ * The new value for the tape item.
+ */
+ @Override
+ public void item(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.item(itm);
+ }
+
+ /**
+ * Get the current number of elements in the tape.
+ *
+ * @return The current number of elements in the tape.
+ */
+ @Override
+ public int size() {
+ if (currentTape == null) return 0;
+
+ return currentTape.size();
+ }
+
+ @Override
+ public int position() {
+ if (currentTape == null) return 0;
+
+ return currentTape.position();
+ }
+
+ /**
+ * Insert an element before the current item.
+ *
+ * @param itm
+ * The item to add.
+ */
+ @Override
+ public void insertBefore(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.insertBefore(itm);
+ }
+
+ /**
+ * Insert an element after the current item.
+ */
+ @Override
+ public void insertAfter(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.insertAfter(itm);
+ }
+
+ /**
+ * Remove the current element.
+ *
+ * Also moves the cursor back one step if possible to maintain relative
+ * position, and removes the corresponding item from the non-active side
+ *
+ * @return The removed item from the active side.
+ */
+ @Override
+ public T remove() {
+ if (currentTape == null) return null;
+
+ return currentTape.remove();
+ }
+
+ /**
+ * Move the cursor to the left-most position.
+ */
+ @Override
+ public void first() {
+ if (currentTape == null) return;
+
+ currentTape.first();
+ }
+
+ /**
+ * Move the cursor the right-most position.
+ */
+ @Override
+ public void last() {
+ if (currentTape == null) return;
+
+ currentTape.last();
+ }
+
+ /**
+ * Move the cursor one space left.
+ *
+ * The cursor can't go past zero.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left() {
+ return left(1);
+ }
+
+ /**
+ * Move the cursor the specified amount left.
+ *
+ * The cursor can't go past zero. Attempts to move the cursor by amounts
+ * that would exceed zero don't move the cursor at all.
+ *
+ * @param amt
+ * The amount to attempt to move the cursor left.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left(final int amt) {
+ if (currentTape == null) return false;
+
+ return currentTape.left(amt);
+ }
+
+ /**
+ * Move the cursor one space right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right() {
+ return right(1);
+ }
+
+ /**
+ * Move the cursor the specified amount right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @param amt
+ * The amount to move the cursor right by.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right(final int amt) {
+ if (currentTape == null) return false;
+
+ return currentTape.right(amt);
+ }
+
+ /**
+ * Flips the tape.
+ *
+ * The active side becomes inactive, and the inactive side becomes
+ * active.
+ *
+ * If the current tape is not double-sided, does nothing.
+ */
+ public void flip() {
+ if (currentTape == null) return;
+
+ if (currentTape.isDoubleSided()) {
+ ((DoubleTape<T>) currentTape).flip();
+ }
+ }
+
+ @Override
+ public boolean isDoubleSided() {
+ if (currentTape == null) return false;
+
+ return currentTape.isDoubleSided();
+ }
+
+ /**
+ * Check if a tape is currently loaded.
+ *
+ * @return Whether or not a tape is loaded.
+ */
+ public boolean isLoaded() {
+ return currentTape != null;
+ }
+
+ /**
+ * Move to the next tape in the changer.
+ *
+ * Attempting to load a tape that isn't there won't eject the current
+ * tape.
+ *
+ * @return Whether or not the next tape was loaded.
+ */
+ public boolean nextTape() {
+ final boolean succ = tapes.right();
+
+ if (succ) {
+ currentTape = tapes.item();
+ }
+
+ return succ;
+ }
+
+ /**
+ * Move to the previous tape in the changer.
+ *
+ * Attempting to load a tape that isn't there won't eject the current
+ * tape.
+ *
+ * @return Whether or not the previous tape was loaded.
+ */
+ public boolean prevTape() {
+ final boolean succ = tapes.left();
+
+ if (succ) {
+ currentTape = tapes.item();
+ }
+
+ return succ;
+ }
+
+ /**
+ * Inserts a tape into the tape changer.
+ *
+ * Any currently loaded tape is ejected, and becomes the previous tape.
+ *
+ * The specified tape is loaded.
+ *
+ * @param tp
+ * The tape to insert and load.
+ */
+ public void insertTape(final Tape<T> tp) {
+ tapes.insertAfter(tp);
+ tapes.right();
+
+ currentTape = tapes.item();
+ }
+
+ /**
+ * Removes the current tape.
+ *
+ * Does nothing if there is not a tape loaded.
+ *
+ * Loads the previous tape, if there is one.
+ *
+ * @return The removed tape.
+ */
+ public Tape<T> removeTape() {
+ if (currentTape == null) return null;
+
+ final Tape<T> tp = tapes.remove();
+ currentTape = tapes.item();
+
+ return tp;
+ }
+
+ /**
+ * Ejects the current tape.
+ *
+ * Does nothing if no tape is loaded.
+ */
+ public void eject() {
+ currentTape = null;
+ }
+
+ /**
+ * Get how many tapes are currently in the changer.
+ *
+ * @return How many tapes are currently in the changer.
+ */
+ public int tapeCount() {
+ return tapes.size();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (currentTape == null ? 0 : currentTape.hashCode());
+ result = prime * result + (tapes == null ? 0 : tapes.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof TapeChanger<?>)) return false;
+
+ final TapeChanger<?> other = (TapeChanger<?>) obj;
+
+ if (currentTape == null) {
+ if (other.currentTape != null) return false;
+ } else if (!currentTape.equals(other.currentTape)) return false;
+
+ if (tapes == null) {
+ if (other.tapes != null) return false;
+ } else if (!tapes.equals(other.tapes)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TapeChanger [tapes=%s, currentTape='%s']", tapes, currentTape);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/TapeLibrary.java b/base/src/main/java/bjc/utils/esodata/TapeLibrary.java
new file mode 100644
index 0000000..2dbc70b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/TapeLibrary.java
@@ -0,0 +1,340 @@
+package bjc.utils.esodata;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A tape changer is essentially a map of tapes.
+ *
+ * It has a current tape that you can do operations to, but also operations to
+ * add/remove other tapes.
+ *
+ * If there is no tape currently loaded into the changer, all the methods will
+ * either return null/false.
+ *
+ * @param <T>
+ * The element type of the tapes.
+ */
+public class TapeLibrary<T> implements Tape<T> {
+ private final Map<String, Tape<T>> tapes;
+ private Tape<T> currentTape;
+
+ /**
+ * Create a new empty tape library.
+ */
+ public TapeLibrary() {
+ tapes = new HashMap<>();
+ }
+
+ /**
+ * Get the item the tape is currently on.
+ *
+ * @return The item the tape is on.
+ */
+ @Override
+ public T item() {
+ if (currentTape == null) return null;
+
+ return currentTape.item();
+ }
+
+ /**
+ * Set the item the tape is currently on.
+ *
+ * @param itm
+ * The new value for the tape item.
+ */
+ @Override
+ public void item(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.item(itm);
+ }
+
+ /**
+ * Get the current number of elements in the tape.
+ *
+ * @return The current number of elements in the tape.
+ */
+ @Override
+ public int size() {
+ if (currentTape == null) return 0;
+
+ return currentTape.size();
+ }
+
+ @Override
+ public int position() {
+ if (currentTape == null) return 0;
+
+ return currentTape.position();
+ }
+ /**
+ * Insert an element before the current item.
+ *
+ * @param itm
+ * The item to add.
+ */
+ @Override
+ public void insertBefore(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.insertBefore(itm);
+ }
+
+ /**
+ * Insert an element after the current item.
+ */
+ @Override
+ public void insertAfter(final T itm) {
+ if (currentTape == null) return;
+
+ currentTape.insertAfter(itm);
+ }
+
+ /**
+ * Remove the current element.
+ *
+ * Also moves the cursor back one step if possible to maintain relative
+ * position, and removes the corresponding item from the non-active side
+ *
+ * @return The removed item from the active side.
+ */
+ @Override
+ public T remove() {
+ if (currentTape == null) return null;
+
+ return currentTape.remove();
+ }
+
+ /**
+ * Move the cursor to the left-most position.
+ */
+ @Override
+ public void first() {
+ if (currentTape == null) return;
+
+ currentTape.first();
+ }
+
+ /**
+ * Move the cursor the right-most position.
+ */
+ @Override
+ public void last() {
+ if (currentTape == null) return;
+
+ currentTape.last();
+ }
+
+ /**
+ * Move the cursor one space left.
+ *
+ * The cursor can't go past zero.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left() {
+ return left(1);
+ }
+
+ /**
+ * Move the cursor the specified amount left.
+ *
+ * The cursor can't go past zero. Attempts to move the cursor by amounts
+ * that would exceed zero don't move the cursor at all.
+ *
+ * @param amt
+ * The amount to attempt to move the cursor left.
+ *
+ * @return True if the cursor was moved left.
+ */
+ @Override
+ public boolean left(final int amt) {
+ if (currentTape == null) return false;
+
+ return currentTape.left(amt);
+ }
+
+ /**
+ * Move the cursor one space right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right() {
+ return right(1);
+ }
+
+ /**
+ * Move the cursor the specified amount right.
+ *
+ * Moving the cursor right will auto-extend the tape if that is enabled.
+ *
+ * @param amt
+ * The amount to move the cursor right by.
+ *
+ * @return Whether the cursor was moved right.
+ */
+ @Override
+ public boolean right(final int amt) {
+ if (currentTape == null) return false;
+
+ return currentTape.right(amt);
+ }
+
+ /**
+ * Flips the tape.
+ *
+ * The active side becomes inactive, and the inactive side becomes
+ * active.
+ *
+ * If the current tape is not double-sided, does nothing.
+ */
+ public void flip() {
+ if (currentTape == null) return;
+
+ if (currentTape.isDoubleSided()) {
+ ((DoubleTape<T>) currentTape).flip();
+ }
+ }
+
+ @Override
+ public boolean isDoubleSided() {
+ if (currentTape == null) return false;
+
+ return currentTape.isDoubleSided();
+ }
+
+ /**
+ * Check if a tape is currently loaded.
+ *
+ * @return Whether or not a tape is loaded.
+ */
+ public boolean isLoaded() {
+ return currentTape != null;
+ }
+
+ /**
+ * Move to the specified tape in the library.
+ *
+ * Attempting to load a tape that isn't there won't eject the current
+ * tape.
+ *
+ * @param label
+ * The label of the tape to load.
+ *
+ * @return Whether or not the next tape was loaded.
+ */
+ public boolean switchTape(final String label) {
+ if (tapes.containsKey(label)) {
+ currentTape = tapes.get(label);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Inserts a tape into the tape library.
+ *
+ * Any currently loaded tape is ejected.
+ *
+ * The specified tape is loaded.
+ *
+ * Adding a duplicate tape will overwrite any existing types.
+ *
+ * @param label
+ * The label of the tape to add.
+ *
+ * @param tp
+ * The tape to insert and load.
+ */
+ public void insertTape(final String label, final Tape<T> tp) {
+ tapes.put(label, tp);
+
+ currentTape = tp;
+ }
+
+ /**
+ * Remove a tape from the library.
+ *
+ * Does nothing if there is not a tape of that name loaded.
+ *
+ * @param label
+ * The tape to remove.
+ *
+ * @return The removed tape.
+ */
+ public Tape<T> removeTape(final String label) {
+ return tapes.remove(label);
+ }
+
+ /**
+ * Ejects the current tape.
+ *
+ * Does nothing if no tape is loaded.
+ */
+ public void eject() {
+ currentTape = null;
+ }
+
+ /**
+ * Get how many tapes are currently in the library.
+ *
+ * @return How many tapes are currently in the library.
+ */
+ public int tapeCount() {
+ return tapes.size();
+ }
+
+ /**
+ * Check if a specific tape is loaded into the library.
+ *
+ * @param label
+ * The tape to check for.
+ *
+ * @return Whether or not a tape of that name exists
+ */
+ public boolean hasTape(final String label) {
+ return tapes.containsKey(label);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (currentTape == null ? 0 : currentTape.hashCode());
+ result = prime * result + (tapes == null ? 0 : tapes.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof TapeLibrary<?>)) return false;
+
+ final TapeLibrary<?> other = (TapeLibrary<?>) obj;
+
+ if (currentTape == null) {
+ if (other.currentTape != null) return false;
+ } else if (!currentTape.equals(other.currentTape)) return false;
+
+ if (tapes == null) {
+ if (other.tapes != null) return false;
+ } else if (!tapes.equals(other.tapes)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TapeLibrary [tapes=%s, currentTape='%s']", tapes, currentTape);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java b/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java
new file mode 100644
index 0000000..ffb639f
--- /dev/null
+++ b/base/src/main/java/bjc/utils/esodata/UnifiedDirectory.java
@@ -0,0 +1,105 @@
+package bjc.utils.esodata;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * Simple implementation of {@link Directory}.
+ *
+ * Has a unified namespace for data and children.
+ *
+ * @author EVE
+ *
+ * @param <K>
+ * The key type of the directory.
+ * @param <V>
+ * The value type of the directory.
+ */
+public class UnifiedDirectory<K, V> implements Directory<K, V> {
+ private final IMap<K, Directory<K, V>> children;
+
+ private final IMap<K, V> data;
+
+ /**
+ * Create a new directory.
+ */
+ public UnifiedDirectory() {
+ children = new FunctionalMap<>();
+ data = new FunctionalMap<>();
+ }
+
+ @Override
+ public Directory<K, V> getSubdirectory(final K key) {
+ return children.get(key);
+ }
+
+ @Override
+ public boolean hasSubdirectory(final K key) {
+ return children.containsKey(key);
+ }
+
+ @Override
+ public Directory<K, V> putSubdirectory(final K key, final Directory<K, V> val) {
+ if (data.containsKey(key)) {
+ final String msg = String.format("Key %s is already used for data", key);
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ return children.put(key, val);
+ }
+
+ @Override
+ public boolean containsKey(final K key) {
+ return data.containsKey(key);
+ }
+
+ @Override
+ public V getKey(final K key) {
+ return data.get(key);
+ }
+
+ @Override
+ public V putKey(final K key, final V val) {
+ if (children.containsKey(key)) {
+ final String msg = String.format("Key %s is already used for sub-directories.", key);
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ return data.put(key, val);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (children == null ? 0 : children.hashCode());
+ result = prime * result + (data == null ? 0 : data.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof UnifiedDirectory<?, ?>)) return false;
+
+ final UnifiedDirectory<?, ?> other = (UnifiedDirectory<?, ?>) obj;
+
+ if (children == null) {
+ if (other.children != null) return false;
+ } else if (!children.equals(other.children)) return false;
+
+ if (data == null) {
+ if (other.data != null) return false;
+ } else if (!data.equals(other.data)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UnifiedDirectory [children=%s, data=%s]", children, data);
+ }
+} \ No newline at end of file