summaryrefslogtreecommitdiff
path: root/base/src/main/java
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
parentb3ac1c8690c3e14c879913e5dcc03a5f5e14876e (diff)
Start splitting into maven modules
Diffstat (limited to 'base/src/main/java')
-rw-r--r--base/src/main/java/bjc/utils/PropertyDB.java160
-rw-r--r--base/src/main/java/bjc/utils/cli/CLICommander.java134
-rw-r--r--base/src/main/java/bjc/utils/cli/Command.java39
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandHandler.java24
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandHelp.java31
-rw-r--r--base/src/main/java/bjc/utils/cli/CommandMode.java72
-rw-r--r--base/src/main/java/bjc/utils/cli/DelegatingCommand.java64
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericCommand.java84
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericCommandMode.java469
-rw-r--r--base/src/main/java/bjc/utils/cli/GenericHelp.java63
-rw-r--r--base/src/main/java/bjc/utils/cli/NullHelp.java20
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java392
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/Command.java87
-rw-r--r--base/src/main/java/bjc/utils/cli/objects/DefineCLI.java133
-rw-r--r--base/src/main/java/bjc/utils/components/ComponentDescription.java135
-rw-r--r--base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java65
-rw-r--r--base/src/main/java/bjc/utils/components/ComponentDescriptionState.java144
-rw-r--r--base/src/main/java/bjc/utils/components/FileComponentRepository.java181
-rw-r--r--base/src/main/java/bjc/utils/components/IComponentRepository.java49
-rw-r--r--base/src/main/java/bjc/utils/components/IDescribedComponent.java64
-rw-r--r--base/src/main/java/bjc/utils/data/BooleanToggle.java76
-rw-r--r--base/src/main/java/bjc/utils/data/CircularIterator.java81
-rw-r--r--base/src/main/java/bjc/utils/data/Either.java173
-rw-r--r--base/src/main/java/bjc/utils/data/GeneratingIterator.java53
-rw-r--r--base/src/main/java/bjc/utils/data/IHolder.java153
-rw-r--r--base/src/main/java/bjc/utils/data/IPair.java200
-rw-r--r--base/src/main/java/bjc/utils/data/ITree.java234
-rw-r--r--base/src/main/java/bjc/utils/data/Identity.java118
-rw-r--r--base/src/main/java/bjc/utils/data/Lazy.java194
-rw-r--r--base/src/main/java/bjc/utils/data/LazyPair.java240
-rw-r--r--base/src/main/java/bjc/utils/data/ListHolder.java104
-rw-r--r--base/src/main/java/bjc/utils/data/Option.java93
-rw-r--r--base/src/main/java/bjc/utils/data/Pair.java135
-rw-r--r--base/src/main/java/bjc/utils/data/SingleIterator.java41
-rw-r--r--base/src/main/java/bjc/utils/data/SingleSupplier.java72
-rw-r--r--base/src/main/java/bjc/utils/data/Toggle.java35
-rw-r--r--base/src/main/java/bjc/utils/data/TopDownTransformIterator.java208
-rw-r--r--base/src/main/java/bjc/utils/data/TopDownTransformResult.java34
-rw-r--r--base/src/main/java/bjc/utils/data/TransformIterator.java46
-rw-r--r--base/src/main/java/bjc/utils/data/Tree.java390
-rw-r--r--base/src/main/java/bjc/utils/data/ValueToggle.java54
-rw-r--r--base/src/main/java/bjc/utils/data/internals/BoundLazy.java145
-rw-r--r--base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java199
-rw-r--r--base/src/main/java/bjc/utils/data/internals/BoundListHolder.java68
-rw-r--r--base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java149
-rw-r--r--base/src/main/java/bjc/utils/data/internals/WrappedLazy.java62
-rw-r--r--base/src/main/java/bjc/utils/data/internals/WrappedOption.java76
-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
-rw-r--r--base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java31
-rw-r--r--base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java31
-rw-r--r--base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java25
-rw-r--r--base/src/main/java/bjc/utils/funcdata/ExtendedMap.java127
-rw-r--r--base/src/main/java/bjc/utils/funcdata/FunctionalList.java423
-rw-r--r--base/src/main/java/bjc/utils/funcdata/FunctionalMap.java175
-rw-r--r--base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java159
-rw-r--r--base/src/main/java/bjc/utils/funcdata/IList.java416
-rw-r--r--base/src/main/java/bjc/utils/funcdata/IMap.java188
-rw-r--r--base/src/main/java/bjc/utils/funcdata/SentryList.java41
-rw-r--r--base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java102
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java221
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java119
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java287
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java49
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java96
-rw-r--r--base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java25
-rw-r--r--base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java139
-rw-r--r--base/src/main/java/bjc/utils/funcdata/theory/Functor.java39
-rw-r--r--base/src/main/java/bjc/utils/functypes/ID.java20
-rw-r--r--base/src/main/java/bjc/utils/functypes/ListFlattener.java17
-rw-r--r--base/src/main/java/bjc/utils/funcutils/CollectorUtils.java39
-rw-r--r--base/src/main/java/bjc/utils/funcutils/CompoundCollector.java89
-rw-r--r--base/src/main/java/bjc/utils/funcutils/EnumUtils.java63
-rw-r--r--base/src/main/java/bjc/utils/funcutils/FileUtils.java40
-rw-r--r--base/src/main/java/bjc/utils/funcutils/FuncUtils.java76
-rw-r--r--base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java36
-rw-r--r--base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java62
-rw-r--r--base/src/main/java/bjc/utils/funcutils/IBuilder.java31
-rw-r--r--base/src/main/java/bjc/utils/funcutils/Isomorphism.java60
-rw-r--r--base/src/main/java/bjc/utils/funcutils/LambdaLock.java105
-rw-r--r--base/src/main/java/bjc/utils/funcutils/ListUtils.java294
-rw-r--r--base/src/main/java/bjc/utils/funcutils/NumberUtils.java69
-rw-r--r--base/src/main/java/bjc/utils/funcutils/StringUtils.java196
-rw-r--r--base/src/main/java/bjc/utils/funcutils/TreeUtils.java56
-rw-r--r--base/src/main/java/bjc/utils/funcutils/TriConsumer.java31
-rw-r--r--base/src/main/java/bjc/utils/gen/RandomGrammar.java69
-rw-r--r--base/src/main/java/bjc/utils/gen/WeightedGrammar.java573
-rw-r--r--base/src/main/java/bjc/utils/gen/WeightedRandom.java112
-rw-r--r--base/src/main/java/bjc/utils/graph/AdjacencyMap.java216
-rw-r--r--base/src/main/java/bjc/utils/graph/Edge.java112
-rw-r--r--base/src/main/java/bjc/utils/graph/Graph.java267
-rw-r--r--base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java56
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleDialogs.java269
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleFileChooser.java198
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java208
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java40
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleJList.java49
-rw-r--r--base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java25
-rw-r--r--base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java35
-rw-r--r--base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java50
-rw-r--r--base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java144
-rw-r--r--base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java22
-rw-r--r--base/src/main/java/bjc/utils/gui/layout/HLayout.java25
-rw-r--r--base/src/main/java/bjc/utils/gui/layout/VLayout.java25
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java73
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java66
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java79
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java133
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java45
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java93
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java42
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java187
-rw-r--r--base/src/main/java/bjc/utils/gui/panels/package-info.java5
-rw-r--r--base/src/main/java/bjc/utils/ioutils/CLFormatter.java531
-rw-r--r--base/src/main/java/bjc/utils/ioutils/CLParameters.java109
-rw-r--r--base/src/main/java/bjc/utils/ioutils/NumberUtils.java405
-rw-r--r--base/src/main/java/bjc/utils/ioutils/Prompter.java47
-rw-r--r--base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java230
-rw-r--r--base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java265
-rw-r--r--base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java100
-rw-r--r--base/src/main/java/bjc/utils/ioutils/SimpleProperties.java170
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/Block.java88
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java73
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java81
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java61
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java97
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java86
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java81
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java54
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java106
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java102
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java115
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java63
-rw-r--r--base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java70
-rw-r--r--base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java46
-rw-r--r--base/src/main/java/bjc/utils/parserutils/IPrecedent.java28
-rw-r--r--base/src/main/java/bjc/utils/parserutils/ParserException.java36
-rw-r--r--base/src/main/java/bjc/utils/parserutils/ShuntingYard.java274
-rw-r--r--base/src/main/java/bjc/utils/parserutils/StringDescaper.java242
-rw-r--r--base/src/main/java/bjc/utils/parserutils/TokenTransformer.java131
-rw-r--r--base/src/main/java/bjc/utils/parserutils/TokenUtils.java303
-rw-r--r--base/src/main/java/bjc/utils/parserutils/TreeConstructor.java125
-rw-r--r--base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java48
-rw-r--r--base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java23
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java21
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java593
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java33
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java54
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java93
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java371
-rw-r--r--base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java31
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java50
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java122
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java71
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java37
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java46
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java21
-rw-r--r--base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java38
170 files changed, 21566 insertions, 0 deletions
diff --git a/base/src/main/java/bjc/utils/PropertyDB.java b/base/src/main/java/bjc/utils/PropertyDB.java
new file mode 100644
index 0000000..713e1e0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/PropertyDB.java
@@ -0,0 +1,160 @@
+package bjc.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.regex.Pattern;
+
+import bjc.utils.funcutils.LambdaLock;
+import bjc.utils.ioutils.SimpleProperties;
+
+/**
+ * Database for storage of properties from external files.
+ *
+ * @author EVE
+ *
+ */
+public class PropertyDB {
+ private static SimpleProperties regexes;
+ private static Map<String, Pattern> compiledRegexes;
+
+ private static SimpleProperties formats;
+
+ /*
+ * Whether or not to log during the loading.
+ */
+ private static final boolean LOGLOAD = false;
+
+ /*
+ * The lock to use to ensure a read can't happen during a reload
+ */
+ private static LambdaLock loadLock = new LambdaLock();
+
+ static {
+ reloadProperties();
+ }
+
+ /**
+ * Reload all the properties from their files.
+ *
+ * NOTE: Any attempts to read from the property DB while properties are
+ * being loaded will block, to prevent reads from partial states.
+ */
+ public static void reloadProperties() {
+ /*
+ * Do the load with the write lock taken.
+ */
+ loadLock.write(() -> {
+ if (LOGLOAD) {
+ System.out.println("Reading regex properties:");
+ }
+
+ /*
+ * Load regexes.
+ */
+ regexes = new SimpleProperties();
+ regexes.loadFrom(PropertyDB.class.getResourceAsStream("/regexes.sprop"), false);
+ if (LOGLOAD) {
+ regexes.outputProperties();
+ System.out.println();
+ }
+ compiledRegexes = new HashMap<>();
+
+ if (LOGLOAD) {
+ System.out.println("Reading format properties:");
+ }
+
+ /*
+ * Load formats.
+ */
+ formats = new SimpleProperties();
+ formats.loadFrom(PropertyDB.class.getResourceAsStream("/formats.sprop"), false);
+ if (LOGLOAD) {
+ formats.outputProperties();
+ System.out.println();
+ }
+ });
+ }
+
+ /**
+ * Retrieve a persisted regular expression.
+ *
+ * @param key
+ * The name of the regular expression.
+ *
+ * @return The regular expression with that name.
+ */
+ public static String getRegex(final String key) {
+ return loadLock.read(() -> {
+ if (!regexes.containsKey(key)) {
+ final String msg = String.format("No regular expression named '%s' found", key);
+
+ throw new NoSuchElementException(msg);
+ }
+
+ return regexes.get(key);
+ });
+ }
+
+ /**
+ * Retrieve a persisted regular expression, compiled into a regular
+ * expression.
+ *
+ * @param key
+ * The name of the regular expression.
+ *
+ * @return The regular expression with that name.
+ */
+ public static Pattern getCompiledRegex(final String key) {
+ return loadLock.read(() -> {
+ if (!regexes.containsKey(key)) {
+ final String msg = String.format("No regular expression named '%s' found", key);
+
+ throw new NoSuchElementException(msg);
+ }
+
+ /*
+ * Get the regex, and cache a compiled version.
+ */
+ return compiledRegexes.computeIfAbsent(key, strang -> {
+ return Pattern.compile(regexes.get(strang));
+ });
+ });
+ }
+
+ /**
+ * Retrieve a persisted format string.
+ *
+ * @param key
+ * The name of the format string.
+ *
+ * @return The format string with that name.
+ */
+ public static String getFormat(final String key) {
+ return loadLock.read(() -> {
+ if (!formats.containsKey(key)) {
+ final String msg = String.format("No format string named '%s' found", key);
+
+ throw new NoSuchElementException(msg);
+ }
+
+ return formats.get(key);
+ });
+ }
+
+ /**
+ * Retrieve a persisted format string, and apply it to a set of
+ * arguments.
+ *
+ * @param key
+ * The name of the format string.
+ *
+ * @param objects
+ * The parameters to the format string.
+ *
+ * @return The format string with that name.
+ */
+ public static String applyFormat(final String key, final Object... objects) {
+ return String.format(getFormat(key), objects);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/CLICommander.java b/base/src/main/java/bjc/utils/cli/CLICommander.java
new file mode 100644
index 0000000..cccb255
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CLICommander.java
@@ -0,0 +1,134 @@
+package bjc.utils.cli;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Scanner;
+
+/**
+ * Runs a CLI interface from the provided set of streams.
+ *
+ * @author ben
+ *
+ */
+public class CLICommander {
+ /*
+ * The streams used for input and normal/error output.
+ */
+ private final InputStream input;
+ private final OutputStream output;
+ private final OutputStream error;
+
+ /*
+ * The command mode to start execution in.
+ */
+ private CommandMode initialMode;
+
+ /**
+ * Create a new CLI interface powered by streams.
+ *
+ * @param input
+ * The stream to get user input from.
+ * @param output
+ * The stream to send normal output to.
+ * @param error
+ * The stream to send error output to.
+ */
+ public CLICommander(final InputStream input, final OutputStream output, final OutputStream error) {
+ if (input == null)
+ throw new NullPointerException("Input stream must not be null");
+ else if (output == null)
+ throw new NullPointerException("Output stream must not be null");
+ else if (error == null) throw new NullPointerException("Error stream must not be null");
+
+ this.input = input;
+ this.output = output;
+ this.error = error;
+ }
+
+ /**
+ * Start handling commands from the given input stream.
+ */
+ public void runCommands() {
+ /*
+ * Setup output streams.
+ */
+ final PrintStream normalOutput = new PrintStream(output);
+ final PrintStream errorOutput = new PrintStream(error);
+
+ /*
+ * Set up input streams.
+ *
+ * We're suppressing the warning because we might use the input
+ * stream multiple times.
+ */
+ @SuppressWarnings("resource")
+ final Scanner inputSource = new Scanner(input);
+
+ /*
+ * The mode currently being used to handle commands.
+ *
+ * Used to preserve the initial mode.
+ */
+ CommandMode currentMode = initialMode;
+
+ /*
+ * Process commands until we're told to stop.
+ */
+ while (currentMode != null) {
+ /*
+ * Print out the command prompt, using a custom prompt
+ * if one is specified.
+ */
+ if (currentMode.isCustomPromptEnabled()) {
+ normalOutput.print(currentMode.getCustomPrompt());
+ } else {
+ normalOutput.print(currentMode.getName() + ">> ");
+ }
+
+ /*
+ * Read in a command.
+ */
+ final String currentLine = inputSource.nextLine();
+
+ /*
+ * Handle commands we can handle.
+ */
+ if (currentMode.canHandle(currentLine)) {
+ final String[] commandTokens = currentLine.split(" ");
+ String[] commandArgs = null;
+
+ final int argCount = commandTokens.length;
+
+ /*
+ * Parse args if they are present.
+ */
+ if (argCount > 1) {
+ commandArgs = Arrays.copyOfRange(commandTokens, 1, argCount);
+ }
+
+ /*
+ * Process command.
+ */
+ currentMode = currentMode.process(commandTokens[0], commandArgs);
+ } else {
+ errorOutput.print("Error: Unrecognized command " + currentLine);
+ }
+ }
+
+ normalOutput.print("Exiting now.");
+ }
+
+ /**
+ * Set the initial command mode to use.
+ *
+ * @param initialMode
+ * The initial command mode to use.
+ */
+ public void setInitialCommandMode(final CommandMode initialMode) {
+ if (initialMode == null) throw new NullPointerException("Initial mode must be non-zero");
+
+ this.initialMode = initialMode;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/Command.java b/base/src/main/java/bjc/utils/cli/Command.java
new file mode 100644
index 0000000..02bc061
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/Command.java
@@ -0,0 +1,39 @@
+package bjc.utils.cli;
+
+/**
+ * Represents a command that can be invoked from a {@link CommandMode}
+ *
+ * @author ben
+ *
+ */
+public interface Command {
+ /**
+ * Create a command that serves as an alias to this one
+ *
+ * @return A command that serves as an alias to this one
+ */
+ Command aliased();
+
+ /**
+ * Get the handler that executes this command
+ *
+ * @return The handler that executes this command
+ */
+ CommandHandler getHandler();
+
+ /**
+ * Get the help entry for this command
+ *
+ * @return The help entry for this command
+ */
+ CommandHelp getHelp();
+
+ /**
+ * Check if this command is an alias of another command
+ *
+ * @return Whether or not this command is an alias of another
+ */
+ default boolean isAlias() {
+ return false;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandHandler.java b/base/src/main/java/bjc/utils/cli/CommandHandler.java
new file mode 100644
index 0000000..2548248
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandHandler.java
@@ -0,0 +1,24 @@
+package bjc.utils.cli;
+
+import java.util.function.Function;
+
+/**
+ * A handler for a command
+ *
+ * @author ben
+ *
+ */
+@FunctionalInterface
+public interface CommandHandler extends Function<String[], CommandMode> {
+ /**
+ * Execute this command
+ *
+ * @param args
+ * The arguments for this command
+ * @return The command mode to switch to after this command, or null to
+ * stop executing commands
+ */
+ default CommandMode handle(final String[] args) {
+ return this.apply(args);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandHelp.java b/base/src/main/java/bjc/utils/cli/CommandHelp.java
new file mode 100644
index 0000000..86567a0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandHelp.java
@@ -0,0 +1,31 @@
+package bjc.utils.cli;
+
+/**
+ * Interface for the help entry for a command
+ *
+ * @author ben
+ *
+ */
+public interface CommandHelp {
+ /**
+ * Get the description of a command.
+ *
+ * @return The description of a command
+ */
+ String getDescription();
+
+ /**
+ * Get the summary line for a command.
+ *
+ * A summary line should consist of a string of the following format
+ *
+ * <pre>
+ * "&lt;command-name>\t&lt;command-summary>"
+ * </pre>
+ *
+ * where anything in angle brackets should be filled in.
+ *
+ * @return The summary line line for a command
+ */
+ String getSummary();
+}
diff --git a/base/src/main/java/bjc/utils/cli/CommandMode.java b/base/src/main/java/bjc/utils/cli/CommandMode.java
new file mode 100644
index 0000000..39c72fc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/CommandMode.java
@@ -0,0 +1,72 @@
+package bjc.utils.cli;
+
+/**
+ * A mode for determining the commands that are valid to enter, and then
+ * handling those commands
+ *
+ * @author ben
+ *
+ */
+public interface CommandMode extends Comparable<CommandMode> {
+ /**
+ * Check to see if this mode can handle the specified command
+ *
+ * @param command
+ * The command to check
+ * @return Whether or not this mode can handle the command. It is
+ * assumed not by default
+ */
+ default boolean canHandle(final String command) {
+ return false;
+ };
+
+ /**
+ * Get the custom prompt for this mode
+ *
+ * @return the custom prompt for this mode
+ *
+ * @throws UnsupportedOperationException
+ * if this mode doesn't support a custom prompt
+ */
+ default String getCustomPrompt() {
+ throw new UnsupportedOperationException("This mode doesn't support a custom prompt");
+ }
+
+ /**
+ * Get the name of this command mode
+ *
+ * @return The name of this command mode, which is the empty string by
+ * default
+ */
+ public default String getName() {
+ return "";
+ }
+
+ /**
+ * Check if this mode uses a custom prompt
+ *
+ * @return Whether or not this mode uses a custom prompt
+ */
+ default boolean isCustomPromptEnabled() {
+ return false;
+ }
+
+ /**
+ * Process a command in this mode
+ *
+ * @param command
+ * The command to process
+ * @param args
+ * A list of arguments to the command
+ * @return The command mode to use for the next command. Defaults to
+ * returning this, and doing nothing else
+ */
+ default CommandMode process(final String command, final String[] args) {
+ return this;
+ }
+
+ @Override
+ default int compareTo(final CommandMode o) {
+ return getName().compareTo(o.getName());
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/DelegatingCommand.java b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java
new file mode 100644
index 0000000..acaa3a6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/DelegatingCommand.java
@@ -0,0 +1,64 @@
+package bjc.utils.cli;
+
+/**
+ * A class for a command that delegates to another command.
+ *
+ * @author ben
+ *
+ */
+class DelegatingCommand implements Command {
+ /*
+ * The command to delegate to.
+ */
+ private final Command delegate;
+
+ /**
+ * Create a new command that delegates to another command.
+ *
+ * @param delegate
+ * The command to delegate to.
+ */
+ public DelegatingCommand(final Command delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Command aliased() {
+ return new DelegatingCommand(delegate);
+ }
+
+ @Override
+ public CommandHandler getHandler() {
+ return delegate.getHandler();
+ }
+
+ @Override
+ public CommandHelp getHelp() {
+ return delegate.getHelp();
+ }
+
+ @Override
+ public boolean isAlias() {
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("DelegatingCommand [");
+
+ if (delegate != null) {
+ builder.append("delegate=");
+ builder.append(delegate);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericCommand.java b/base/src/main/java/bjc/utils/cli/GenericCommand.java
new file mode 100644
index 0000000..4ae4dea
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericCommand.java
@@ -0,0 +1,84 @@
+package bjc.utils.cli;
+
+/**
+ * Generic command implementation.
+ *
+ * @author ben
+ *
+ */
+public class GenericCommand implements Command {
+ /*
+ * The behavior for invoking the command.
+ */
+ private final CommandHandler handler;
+
+ /*
+ * The help for the command.
+ */
+ private CommandHelp help;
+
+ /**
+ * Create a new generic command.
+ *
+ * @param handler
+ * The handler to use for the command.
+ * @param description
+ * The description of the command. May be null, in which
+ * case a default is provided.
+ * @param help
+ * The detailed help message for the command. May be
+ * null, in which case the description is repeated for
+ * the detailed help.
+ */
+ public GenericCommand(final CommandHandler handler, final String description, final String help) {
+ if (handler == null) throw new NullPointerException("Command handler must not be null");
+
+ this.handler = handler;
+
+ if (description == null) {
+ this.help = new NullHelp();
+ } else {
+ this.help = new GenericHelp(description, help);
+ }
+ }
+
+ @Override
+ public Command aliased() {
+ return new DelegatingCommand(this);
+ }
+
+ @Override
+ public CommandHandler getHandler() {
+ return handler;
+ }
+
+ @Override
+ public CommandHelp getHelp() {
+ return help;
+ }
+
+ @Override
+ public boolean isAlias() {
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("GenericCommand [");
+
+ if (help != null) {
+ builder.append("help=");
+ builder.append(help);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericCommandMode.java b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
new file mode 100644
index 0000000..8764537
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericCommandMode.java
@@ -0,0 +1,469 @@
+package bjc.utils.cli;
+
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A general command mode, with a customizable set of commands
+ *
+ * There is a small set of commands which is handled by default. The first is
+ * 'list', which lists all the commands the user can input. The second is
+ * 'alias', which allows the user to bind a new name to a command
+ *
+ * @author ben
+ *
+ */
+public class GenericCommandMode implements CommandMode {
+ /*
+ * Contains the commands this mode handles
+ */
+ private final IMap<String, Command> commandHandlers;
+ private final IMap<String, Command> defaultHandlers;
+
+ /*
+ * Contains help topics without an associated command
+ */
+ private final IMap<String, CommandHelp> helpTopics;
+
+ /*
+ * The action to execute upon encountering an unknown command
+ */
+ private BiConsumer<String, String[]> unknownCommandHandler;
+
+ /*
+ * The functions to use for input/output
+ */
+ private final Consumer<String> errorOutput;
+ private final Consumer<String> normalOutput;
+
+ /*
+ * The name of this command mode, or null if it is unnamed
+ */
+ private String modeName;
+
+ /*
+ * The custom prompt to use, or null if none is specified
+ */
+ private String customPrompt;
+
+ /**
+ * Create a new generic command mode
+ *
+ * @param normalOutput
+ * The function to use for normal output
+ * @param errorOutput
+ * The function to use for error output
+ */
+ public GenericCommandMode(final Consumer<String> normalOutput, final Consumer<String> errorOutput) {
+ if (normalOutput == null)
+ throw new NullPointerException("Normal output source must be non-null");
+ else if (errorOutput == null) throw new NullPointerException("Error output source must be non-null");
+
+ this.normalOutput = normalOutput;
+ this.errorOutput = errorOutput;
+
+ /*
+ * Initialize handler maps so that they sort in alphabetical
+ */
+ /*
+ * order
+ */
+ commandHandlers = new FunctionalMap<>(new TreeMap<>());
+ defaultHandlers = new FunctionalMap<>(new TreeMap<>());
+ helpTopics = new FunctionalMap<>(new TreeMap<>());
+
+ setupDefaultCommands();
+ }
+
+ /**
+ * Add an alias to an existing command
+ *
+ * @param commandName
+ * The name of the command to add an alias for
+ * @param aliasName
+ * The new alias for the command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command doesn't have a bound
+ * handler, or if the alias name already has a bound
+ * value
+ */
+ public void addCommandAlias(final String commandName, final String aliasName) {
+ if (commandName == null)
+ throw new NullPointerException("Command name must not be null");
+ else if (aliasName == null)
+ throw new NullPointerException("Alias name must not be null");
+ else if (!commandHandlers.containsKey(commandName) && !defaultHandlers.containsKey(commandName))
+ throw new IllegalArgumentException("Cannot alias non-existant command '" + commandName + "'");
+ else if (commandHandlers.containsKey(aliasName) || defaultHandlers.containsKey(aliasName))
+ throw new IllegalArgumentException(
+ "Cannot bind alias '" + aliasName + "' to a command with a bound handler");
+ else {
+ Command aliasedCommand;
+
+ if (defaultHandlers.containsKey(commandName)) {
+ aliasedCommand = defaultHandlers.get(commandName).aliased();
+ } else {
+ aliasedCommand = commandHandlers.get(commandName).aliased();
+ }
+
+ commandHandlers.put(aliasName, aliasedCommand);
+ }
+ }
+
+ /**
+ * Add a command to this command mode
+ *
+ * @param command
+ * The name of the command to add
+ * @param handler
+ * The handler to use for the specified command
+ *
+ * @throws IllegalArgumentException
+ * if the specified command already has a handler
+ * registered
+ */
+ public void addCommandHandler(final String command, final Command handler) {
+ if (command == null)
+ throw new NullPointerException("Command must not be null");
+ else if (handler == null)
+ throw new NullPointerException("Handler must not be null");
+ else if (canHandle(command))
+ throw new IllegalArgumentException("Command " + command + " already has a handler registered");
+ else {
+ commandHandlers.put(command, handler);
+ }
+ }
+
+ /**
+ * Add a help topic to this command mode that isn't tied to a command
+ *
+ * @param topicName
+ * The name of the topic
+ * @param topic
+ * The contents of the topic
+ */
+ public void addHelpTopic(final String topicName, final CommandHelp topic) {
+ helpTopics.put(topicName, topic);
+ }
+
+ /*
+ * Default command builders
+ */
+
+ private GenericCommand buildAliasCommand() {
+ final String aliasShortHelp = "alias\tAlias one command to another";
+ final String aliasLongHelp = "Gives a command another name it can be invoked by."
+ + " Invoke with two arguments: the name of the command to alias"
+ + "followed by the name of the alias to give that command.";
+
+ return new GenericCommand((args) -> {
+ doAliasCommands(args);
+
+ return this;
+ }, aliasShortHelp, aliasLongHelp);
+ }
+
+ private GenericCommand buildClearCommands() {
+ final String clearShortHelp = "clear\tClear the screen";
+ final String clearLongHelp = "Clears the screen of all the text on it," + " and prints a new prompt.";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support screen clearing");
+
+ return this;
+ }, clearShortHelp, clearLongHelp);
+ }
+
+ private GenericCommand buildExitCommand() {
+ final String exitShortHelp = "exit\tExit the console";
+ final String exitLongHelp = "First prompts the user to make sure they want to"
+ + " exit, then quits if they say they do";
+
+ return new GenericCommand((args) -> {
+ errorOutput.accept("ERROR: This console doesn't support auto-exiting");
+
+ return this;
+ }, exitShortHelp, exitLongHelp);
+ }
+
+ private GenericCommand buildHelpCommand() {
+ final String helpShortHelp = "help\tConsult the help system";
+ final String helpLongHelp = "Consults the internal help system."
+ + " Invoked in two different ways. Invoking with no arguments"
+ + " causes all the topics you can ask for details on to be list,"
+ + " while invoking with the name of a topic will print the entry" + " for that topic";
+
+ return new GenericCommand((args) -> {
+ if (args == null || args.length == 0) {
+ /*
+ * Invoke general help
+ */
+ doHelpSummary();
+ } else {
+ /*
+ * Invoke help for a command
+ */
+ doHelpCommand(args[0]);
+ }
+
+ return this;
+ }, helpShortHelp, helpLongHelp);
+ }
+
+ private GenericCommand buildListCommand() {
+ final String listShortHelp = "list\tList available commands";
+ final String listLongHelp = "Lists all of the commands available in this mode,"
+ + " as well as commands available in any mode";
+
+ return new GenericCommand((args) -> {
+ doListCommands();
+
+ return this;
+ }, listShortHelp, listLongHelp);
+ }
+
+ @Override
+ public boolean canHandle(final String command) {
+ return commandHandlers.containsKey(command) || defaultHandlers.containsKey(command);
+ }
+
+ /*
+ * Implement default commands
+ */
+
+ private void doAliasCommands(final String[] args) {
+ if (args.length != 2) {
+ errorOutput.accept("ERROR: Alias requires two arguments."
+ + " The command name, and the alias for that command");
+ } else {
+ final String commandName = args[0];
+ final String aliasName = args[1];
+
+ if (!canHandle(commandName)) {
+ errorOutput.accept("ERROR: '" + commandName + "' is not a valid command.");
+ } else if (canHandle(aliasName)) {
+ errorOutput.accept("ERROR: Cannot overwrite command '" + aliasName + "'");
+ } else {
+ addCommandAlias(commandName, aliasName);
+ }
+ }
+ }
+
+ private void doHelpCommand(final String commandName) {
+ if (commandHandlers.containsKey(commandName)) {
+ final String desc = commandHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (defaultHandlers.containsKey(commandName)) {
+ final String desc = defaultHandlers.get(commandName).getHelp().getDescription();
+
+ normalOutput.accept("\n" + desc);
+ } else if (helpTopics.containsKey(commandName)) {
+ normalOutput.accept("\n" + helpTopics.get(commandName).getDescription());
+ } else {
+ errorOutput.accept(
+ "ERROR: I'm sorry, but there is no help available for '" + commandName + "'");
+ }
+ }
+
+ private void doHelpSummary() {
+ normalOutput.accept("Help topics for this command mode are as follows:\n");
+
+ if (commandHandlers.size() > 0) {
+ commandHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics available in all command modes are as follows\n");
+ if (defaultHandlers.size() > 0) {
+ defaultHandlers.forEachValue(command -> {
+ if (!command.isAlias()) {
+ normalOutput.accept("\t" + command.getHelp().getSummary() + "\n");
+ }
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+
+ normalOutput.accept("\nHelp topics not associated with a command are as follows\n");
+ if (helpTopics.size() > 0) {
+ helpTopics.forEachValue(topic -> {
+ normalOutput.accept("\t" + topic.getSummary() + "\n");
+ });
+ } else {
+ normalOutput.accept("\tNone available\n");
+ }
+ }
+
+ private void doListCommands() {
+ normalOutput.accept("The available commands for this mode are as follows:\n");
+
+ commandHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\nThe following commands are available in all modes:\n");
+ defaultHandlers.keyList().forEach(commandName -> {
+ normalOutput.accept("\t" + commandName);
+ });
+
+ normalOutput.accept("\n");
+ }
+
+ @Override
+ public String getCustomPrompt() {
+ if (customPrompt != null) return customPrompt;
+
+ return CommandMode.super.getCustomPrompt();
+ }
+
+ @Override
+ public String getName() {
+ if (modeName != null) return modeName;
+
+ return CommandMode.super.getName();
+ }
+
+ @Override
+ public boolean isCustomPromptEnabled() {
+ return customPrompt != null;
+ }
+
+ @Override
+ public CommandMode process(final String command, final String[] args) {
+ normalOutput.accept("\n");
+
+ if (defaultHandlers.containsKey(command))
+ return defaultHandlers.get(command).getHandler().handle(args);
+ else if (commandHandlers.containsKey(command))
+ return commandHandlers.get(command).getHandler().handle(args);
+ else {
+ if (args != null) {
+ errorOutput.accept("ERROR: Unrecognized command " + command + String.join(" ", args));
+ } else {
+ errorOutput.accept("ERROR: Unrecognized command " + command);
+ }
+
+ if (unknownCommandHandler == null)
+ throw new UnsupportedOperationException("Command " + command + " is invalid.");
+
+ unknownCommandHandler.accept(command, args);
+ }
+
+ return this;
+ }
+
+ /**
+ * Set the custom prompt for this mode
+ *
+ * @param prompt
+ * The custom prompt for this mode, or null to disable
+ * the custom prompt
+ */
+ public void setCustomPrompt(final String prompt) {
+ customPrompt = prompt;
+ }
+
+ /**
+ * Set the name of this mode
+ *
+ * @param name
+ * The desired name of this mode, or null to use the
+ * default name
+ */
+ public void setModeName(final String name) {
+ modeName = name;
+ }
+
+ /**
+ * Set the handler to use for unknown commands
+ *
+ * @param handler
+ * The handler to use for unknown commands, or null to
+ * throw on unknown commands
+ */
+ public void setUnknownCommandHandler(final BiConsumer<String, String[]> handler) {
+ if (handler == null) throw new NullPointerException("Handler must not be null");
+
+ unknownCommandHandler = handler;
+ }
+
+ private void setupDefaultCommands() {
+ defaultHandlers.put("list", buildListCommand());
+ defaultHandlers.put("alias", buildAliasCommand());
+ defaultHandlers.put("help", buildHelpCommand());
+
+ addCommandAlias("help", "man");
+
+ /*
+ * Add commands handled in a upper layer.
+ */
+
+ /*
+ * @TODO figure out a place to put commands that apply across
+ */
+ /*
+ * all
+ */
+ /*
+ * modes, but only apply to a specific application
+ */
+ defaultHandlers.put("clear", buildClearCommands());
+ defaultHandlers.put("exit", buildExitCommand());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("GenericCommandMode [");
+
+ if (commandHandlers != null) {
+ builder.append("commandHandlers=");
+ builder.append(commandHandlers);
+ }
+
+ if (defaultHandlers != null) {
+ builder.append(", ");
+ builder.append("defaultHandlers=");
+ builder.append(defaultHandlers);
+ }
+
+ if (helpTopics != null) {
+ builder.append(", ");
+ builder.append("helpTopics=");
+ builder.append(helpTopics);
+ }
+
+ if (modeName != null) {
+ builder.append(", ");
+ builder.append("modeName=");
+ builder.append(modeName);
+ }
+
+ if (customPrompt != null) {
+ builder.append(", ");
+ builder.append("customPrompt=");
+ builder.append(customPrompt);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/cli/GenericHelp.java b/base/src/main/java/bjc/utils/cli/GenericHelp.java
new file mode 100644
index 0000000..38adf57
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/GenericHelp.java
@@ -0,0 +1,63 @@
+package bjc.utils.cli;
+
+/**
+ * Generic implementation of a help topic
+ *
+ * @author ben
+ *
+ */
+public class GenericHelp implements CommandHelp {
+ // The strings for this help topic
+ private final String summary;
+ private final String description;
+
+ /**
+ * Create a new help topic
+ *
+ * @param summary
+ * The summary of this help topic
+ * @param description
+ * The description of this help topic, or null if this
+ * help topic doesn't have a more detailed description
+ */
+ public GenericHelp(final String summary, final String description) {
+ if (summary == null) throw new NullPointerException("Help summary must be non-null");
+
+ this.summary = summary;
+ this.description = description;
+ }
+
+ @Override
+ public String getDescription() {
+ if (description == null) return summary;
+
+ return description;
+ }
+
+ @Override
+ public String getSummary() {
+ return summary;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("GenericHelp [");
+
+ if (summary != null) {
+ builder.append("summary=");
+ builder.append(summary);
+ }
+
+ if (description != null) {
+ builder.append(", ");
+ builder.append("description=");
+ builder.append(description);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/NullHelp.java b/base/src/main/java/bjc/utils/cli/NullHelp.java
new file mode 100644
index 0000000..6c49ae6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/NullHelp.java
@@ -0,0 +1,20 @@
+package bjc.utils.cli;
+
+/**
+ * Implementation of a help topic that doesn't exist
+ *
+ * @author ben
+ *
+ */
+public class NullHelp implements CommandHelp {
+ @Override
+ public String getDescription() {
+ return "No description provided";
+ }
+
+ @Override
+ public String getSummary() {
+ return "No summary provided";
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java
new file mode 100644
index 0000000..ec66fe2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/BlockReaderCLI.java
@@ -0,0 +1,392 @@
+package bjc.utils.cli.objects;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import bjc.utils.ioutils.Prompter;
+import bjc.utils.ioutils.blocks.*;
+
+import static bjc.utils.cli.objects.Command.CommandStatus;
+import static bjc.utils.cli.objects.Command.CommandStatus.*;
+
+public class BlockReaderCLI {
+ private final Logger LOGGER = Logger.getLogger(BlockReaderCLI.class.getName());
+
+ public static class BlockReaderState {
+ public final Map<String, BlockReader> readers;
+ public final Map<String, Reader> sources;
+
+ public BlockReaderState(Map<String, BlockReader> readers, Map<String, Reader> sources) {
+ this.readers = readers;
+ this.sources = sources;
+ }
+ }
+
+ private BlockReaderState stat;
+
+ /**
+ * Create a new CLI for configuring BlockReaders.
+ *
+ * @param srcs
+ * The container of initial I/O sources.
+ */
+ public BlockReaderCLI(Map<String, Reader> srcs) {
+ stat = new BlockReaderState(new HashMap<>(), srcs);
+ }
+
+ public static void main(String[] args) {
+ /*
+ * Create/configure I/O sources.
+ */
+ Map<String, Reader> sources = new HashMap<>();
+ sources.put("stdio", new InputStreamReader(System.in));
+
+ BlockReaderCLI reader = new BlockReaderCLI(sources);
+
+ reader.run(new Scanner(System.in), "console", true);
+ }
+
+ /**
+ * Run the CLI on an input source.
+ *
+ * @param input
+ * The place to read input from.
+ * @param ioSource
+ * The name of the place to read input from.
+ * @param interactive
+ * Whether or not the source is interactive
+ */
+ public void run(Scanner input, String ioSource, boolean interactive) {
+ int lno = 0;
+ while(input.hasNextLine()) {
+ if(interactive)
+ System.out.printf("reader-conf(%d)>", lno);
+
+ String ln = input.nextLine();
+
+ lno += 1;
+
+ Command com = Command.fromString(ln, lno, ioSource);
+ if(com == null) continue;
+
+ CommandStatus stat = handleCommand(com, interactive);
+ if(stat == FINISH || stat == ERROR) {
+ return;
+ }
+ }
+
+ input.close();
+ }
+
+ /*
+ * Handle a command.
+ */
+ public CommandStatus handleCommand(Command com, boolean interactive) {
+ switch(com.nameCommand) {
+ case "def-filtered":
+ return defFiltered(com);
+ case "def-layered":
+ return defLayered(com);
+ case "def-pushback":
+ return defPushback(com);
+ case "def-simple":
+ return defSimple(com);
+ case "def-serial":
+ return defSerial(com);
+ case "def-toggled":
+ return defToggled(com);
+ case "}":
+ case "end":
+ case "exit":
+ case "quit":
+ if(interactive)
+ System.out.printf("Exiting reader-conf, %d readers configured in %d commands\n",
+ stat.readers.size(), com.lineNo);
+ return FINISH;
+ default:
+ LOGGER.severe(com.error("Unknown command '%s'\n", com.nameCommand));
+ return FAIL;
+ }
+ }
+
+ private CommandStatus defFiltered(Command com) {
+ String remn = com.remnCommand;
+
+ /*
+ * Get the block name.
+ */
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No name argument for def-filtered.\n"));
+ return FAIL;
+ }
+ String blockName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get the reader name.
+ */
+ idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No reader-name argument for def-filtered.\n"));
+ return FAIL;
+ }
+ String readerName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there is a reader bound to that name.
+ */
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No source named %s\n", readerName));
+ return FAIL;
+ }
+
+ /*
+ * Get the pattern.
+ */
+ if(remn.equals("")) {
+ LOGGER.severe(com.error("No filter argument for def-filtered\n"));
+ return FAIL;
+ }
+
+ String filter = remn;
+
+ try {
+ Pattern pat = Pattern.compile(filter);
+
+ Predicate<Block> pred = (block) -> {
+ Matcher mat = pat.matcher(block.contents);
+
+ return mat.matches();
+ };
+
+ BlockReader reader = new FilteredBlockReader(stat.readers.get(readerName), pred);
+
+ stat.readers.put(blockName, reader);
+ } catch (PatternSyntaxException psex) {
+ LOGGER.severe(com.error("Invalid regular expression '%s' for filter. (%s)\n", filter, psex.getMessage()));
+ return FAIL;
+ }
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defPushback(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 2) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-pushback. Requires a block name and a reader name\n"));
+ return FAIL;
+ }
+
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader %s\n", blockName));
+ return FAIL;
+ }
+
+ String readerName = parts[1];
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No reader named %s\n", readerName));
+ return FAIL;
+ }
+
+ BlockReader reader = new PushbackBlockReader(stat.readers.get(readerName));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defToggled(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 3) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-toggled. Requires a block name and two reader names\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the block name.
+ */
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Make sure the component readers exist.
+ */
+ if(!stat.readers.containsKey(parts[1])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[1]));
+ return FAIL;
+ }
+
+ if(!stat.readers.containsKey(parts[2])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[2]));
+ return FAIL;
+ }
+
+ BlockReader reader = new ToggledBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2]));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defLayered(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length != 3) {
+ LOGGER.severe(com.error("Incorrect number of arguments to def-layered. Requires a block name and two reader names\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the block name.
+ */
+ String blockName = parts[0];
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Make sure the component readers exist.
+ */
+ if(!stat.readers.containsKey(parts[1])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[1]));
+ return FAIL;
+ }
+
+ if(!stat.readers.containsKey(parts[2])) {
+ LOGGER.severe(com.error("No reader named %s\n", parts[2]));
+ return FAIL;
+ }
+
+ BlockReader reader = new LayeredBlockReader(stat.readers.get(parts[1]), stat.readers.get(parts[2]));
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defSerial(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ if(parts.length < 2) {
+ LOGGER.severe(com.error("Not enough arguments to def-serial. Requires at least a block name and at least one reader name\n"));
+ return FAIL;
+ }
+
+ /*
+ * Get the name for this BlockReader.
+ */
+ String blockName = parts[0];
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get all of the component readers.
+ */
+ BlockReader[] readerArr = new BlockReader[parts.length - 1];
+ for(int i = 1; i < parts.length; i++) {
+ String readerName = parts[i];
+
+ /*
+ * Check there is a reader bound to that name.
+ */
+ if(!stat.readers.containsKey(readerName)) {
+ LOGGER.severe(com.error("No reader named %s\n", readerName));
+ return FAIL;
+ }
+
+ readerArr[i] = stat.readers.get(readerName);
+ }
+
+ BlockReader reader = new SerialBlockReader(readerArr);
+
+ stat.readers.put(blockName, reader);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defSimple(Command com) {
+ String remn = com.remnCommand;
+
+ /*
+ * Get the block name.
+ */
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No name argument for def-simple.\n"));
+ return FAIL;
+ }
+ String blockName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there isn't a reader already bound to this name.
+ */
+ if(stat.readers.containsKey(blockName)) {
+ LOGGER.warning(com.warn("Shadowing existing reader named %s\n", blockName));
+ }
+
+ /*
+ * Get the source name.
+ */
+ idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.severe(com.error("No source-name argument for def-simple.\n"));
+ return FAIL;
+ }
+ String sourceName = remn.substring(0, idx).trim();
+ remn = remn.substring(idx).trim();
+
+ /*
+ * Check there is a source bound to that name.
+ */
+ if(!stat.sources.containsKey(sourceName)) {
+ LOGGER.severe(com.error("No source named %s\n", sourceName));
+ return FAIL;
+ }
+
+ /*
+ * Get the pattern.
+ */
+ if(remn.equals("")) {
+ LOGGER.severe(com.error("No delimiter argument for def-simple\n"));
+ return FAIL;
+ }
+
+ String delim = remn;
+
+ try {
+ BlockReader reader = new SimpleBlockReader(delim, stat.sources.get(sourceName));
+
+ stat.readers.put(blockName, reader);
+ } catch (PatternSyntaxException psex) {
+ LOGGER.severe(com.error("Invalid regular expression '%s' for delimiter. (%s)\n", delim, psex.getMessage()));
+ return FAIL;
+ }
+
+ return SUCCESS;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/Command.java b/base/src/main/java/bjc/utils/cli/objects/Command.java
new file mode 100644
index 0000000..e605a2b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/Command.java
@@ -0,0 +1,87 @@
+package bjc.utils.cli.objects;
+
+public class Command {
+ /**
+ * Command status values.
+ */
+ public static enum CommandStatus {
+ /**
+ * The command succeded.
+ */
+ SUCCESS,
+ /**
+ * The command failed non-fatally.
+ */
+ FAIL,
+ /**
+ * The command failed fatally.
+ */
+ ERROR,
+ /**
+ * The command was the last one.
+ */
+ FINISH,
+ }
+
+ public final int lineNo;
+
+ public final String fullCommand;
+ public final String remnCommand;
+ public final String nameCommand;
+
+ public final String ioSource;
+
+ /**
+ * Create a new command.
+ *
+ * @param ln
+ * The line to get the command from.
+ * @param lno
+ * The number of the line the command came from.
+ * @param ioSrc
+ * The name of where the I/O came from.
+ */
+ public Command(String ln, int lno, String ioSrc) {
+ int idx = ln.indexOf(' ');
+
+ if(idx == -1) idx = ln.length();
+
+ fullCommand = ln;
+ nameCommand = ln.substring(0, idx).trim();
+ remnCommand = ln.substring(idx).trim();
+
+ lineNo = lno;
+
+ ioSource = ioSrc;
+ }
+
+ public static Command fromString(String ln, int lno, String ioSource) {
+ /*
+ * Ignore blank lines and comments.
+ */
+ if(ln.equals("")) return null;
+ if(ln.startsWith("#")) return null;
+
+ /*
+ * Trim off comments part-way through the line.
+ */
+ int idxHash = ln.indexOf('#');
+ if(idxHash != -1) {
+ ln = ln.substring(0, idxHash).trim();
+ }
+
+ return new Command(ln, lno, ioSource);
+ }
+
+ public String warn(String warning, Object... parms) {
+ String msg = String.format(warning, parms);
+
+ return String.format("WARNING (%s:%d): %s", ioSource, lineNo, msg);
+ }
+
+ public String error(String err, Object... parms) {
+ String msg = String.format(err, parms);
+
+ return String.format("ERROR (%s:%d): %s", ioSource, lineNo, msg);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java
new file mode 100644
index 0000000..bb2733f
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/objects/DefineCLI.java
@@ -0,0 +1,133 @@
+package bjc.utils.cli.objects;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.function.UnaryOperator;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import static bjc.utils.cli.objects.Command.CommandStatus;
+import static bjc.utils.cli.objects.Command.CommandStatus.*;
+
+public class DefineCLI {
+ private final Logger LOGGER = Logger.getLogger(DefineCLI.class.getName());
+
+ public static class DefineState {
+ public final Map<String, UnaryOperator<String>> defines;
+
+ public final Map<String, String> strings;
+ public final Map<String, String> formats;
+
+ public final Map<String, Pattern> patterns;
+
+ public DefineState() {
+ this(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
+ }
+
+ public DefineState(Map<String, UnaryOperator<String>> defines,
+ Map<String, String> strings, Map<String, String> formats,
+ Map<String, Pattern> patterns) {
+ this.defines = defines;
+
+ this.strings = strings;
+ this.formats = formats;
+
+ this.patterns = patterns;
+ }
+ }
+
+ private DefineState stat;
+
+ public DefineCLI() {
+ stat = new DefineState();
+ }
+
+ public static void main(String[] args) {
+ DefineCLI defin = new DefineCLI();
+ }
+
+ /**
+ * Run the CLI on an input source.
+ *
+ * @param input
+ * The place to read input from.
+ * @param ioSource
+ * The name of the place to read input from.
+ * @param interactive
+ * Whether or not the source is interactive
+ */
+ public void run(Scanner input, String ioSource, boolean interactive) {
+ int lno = 0;
+ while(input.hasNextLine()) {
+ if(interactive)
+ System.out.printf("define-conf(%d)>", lno);
+
+ String ln = input.nextLine();
+
+ lno += 1;
+
+ Command com = Command.fromString(ln, lno, ioSource);
+ if(com == null) continue;
+
+ handleCommand(com, interactive);
+ }
+
+ input.close();
+ }
+
+ public void handleCommand(Command com, boolean interactive) {
+ switch(com.nameCommand) {
+ case "def-string":
+ default:
+ LOGGER.severe(com.error("Unknown command %s\n", com.nameCommand));
+ break;
+ }
+ }
+
+ private CommandStatus defString(Command com) {
+ String remn = com.remnCommand;
+
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.warning(com.warn("Binding empty string to name '%s'\n", remn));
+ idx = remn.length();
+ }
+ String name = remn.substring(0, idx);
+ String strang = remn.substring(idx);
+
+ if(stat.strings.containsKey(name)) {
+ LOGGER.warning(com.warn("Shadowing string '%s'\n", name));
+ }
+
+ stat.strings.put(name, strang);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus defFormat(Command com) {
+ String remn = com.remnCommand;
+
+ int idx = remn.indexOf(' ');
+ if(idx == -1) {
+ LOGGER.warning(com.warn("Binding empty format to name '%s'\n", remn));
+ idx = remn.length();
+ }
+ String name = remn.substring(0, idx);
+ String fmt = remn.substring(idx);
+
+ if(stat.formats.containsKey(name)) {
+ LOGGER.warning(com.warn("Shadowing format '%s'\n", name));
+ }
+
+ stat.formats.put(name, fmt);
+
+ return SUCCESS;
+ }
+
+ private CommandStatus bindFormat(Command com) {
+ String[] parts = com.remnCommand.split(" ");
+
+ return SUCCESS;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/components/ComponentDescription.java b/base/src/main/java/bjc/utils/components/ComponentDescription.java
new file mode 100644
index 0000000..28f81d1
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/ComponentDescription.java
@@ -0,0 +1,135 @@
+package bjc.utils.components;
+
+/**
+ * Generic implementation of a description for a component
+ *
+ * @author ben
+ *
+ */
+public class ComponentDescription implements IDescribedComponent {
+ private static void sanityCheckArgs(final String name, final String author, final String description,
+ final int version) {
+ if (name == null)
+ throw new NullPointerException("Component name can't be null");
+ else if (version <= 0) throw new IllegalArgumentException("Component version must be greater than 0");
+ }
+
+ /**
+ * The author of the component
+ */
+ private final String author;
+ /**
+ * The description of the component
+ */
+ private final String description;
+ /**
+ * The name of the component
+ */
+ private final String name;
+
+ /**
+ * The version of the component
+ */
+ private final int version;
+
+ /**
+ * Create a new component description
+ *
+ * @param name
+ * The name of the component
+ * @param author
+ * The author of the component
+ * @param description
+ * The description of the component
+ * @param version
+ * The version of the component
+ * @throws IllegalArgumentException
+ * thrown if version is less than 1
+ */
+ public ComponentDescription(final String name, final String author, final String description,
+ final int version) {
+ sanityCheckArgs(name, author, description, version);
+
+ this.name = name;
+ this.author = author;
+ this.description = description;
+ this.version = version;
+ }
+
+ @Override
+ public String getAuthor() {
+ if (author == null) return IDescribedComponent.super.getAuthor();
+
+ return author;
+ }
+
+ @Override
+ public String getDescription() {
+ if (description == null) return IDescribedComponent.super.getDescription();
+
+ return description;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int getVersion() {
+ return version;
+ }
+
+ @Override
+ public String toString() {
+ return name + " component v" + version + ", written by " + author;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (author == null ? 0 : author.hashCode());
+ result = prime * result + (description == null ? 0 : description.hashCode());
+ result = prime * result + (name == null ? 0 : name.hashCode());
+ result = prime * result + version;
+
+ return result;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ final ComponentDescription other = (ComponentDescription) obj;
+
+ if (author == null) {
+ if (other.author != null) return false;
+ } else if (!author.equals(other.author)) return false;
+
+ if (description == null) {
+ if (other.description != null) return false;
+ } else if (!description.equals(other.description)) return false;
+
+ if (name == null) {
+ if (other.name != null) return false;
+ } else if (!name.equals(other.name)) return false;
+
+ if (version != other.version) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java b/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java
new file mode 100644
index 0000000..f7ddaff
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/ComponentDescriptionFileParser.java
@@ -0,0 +1,65 @@
+package bjc.utils.components;
+
+import static bjc.utils.ioutils.RuleBasedReaderPragmas.buildInteger;
+import static bjc.utils.ioutils.RuleBasedReaderPragmas.buildStringCollapser;
+
+import java.io.InputStream;
+
+import bjc.utils.ioutils.RuleBasedConfigReader;
+
+/**
+ * Read a component description from a file
+ *
+ * @author ben
+ *
+ */
+public class ComponentDescriptionFileParser {
+ // The reader used to read in component descriptions
+ private static RuleBasedConfigReader<ComponentDescriptionState> reader;
+
+ // Initialize the reader and its pragmas
+ static {
+ // This reader works entirely off of pragmas, so no need to
+ // handle
+ // rules
+ reader = new RuleBasedConfigReader<>((tokenizer, statePair) -> {
+ // Don't need to do anything on rule start
+ }, (tokenizer, state) -> {
+ // Don't need to do anything on rule continuation
+ }, (state) -> {
+ // Don't need to do anything on rule end
+ });
+
+ setupReaderPragmas();
+ }
+
+ /**
+ * Parse a component description from a stream
+ *
+ * @param inputSource
+ * The stream to parse from
+ * @return The description parsed from the stream
+ */
+ public static ComponentDescription fromStream(final InputStream inputSource) {
+ if (inputSource == null) throw new NullPointerException("Input source must not be null");
+
+ final ComponentDescriptionState readState = reader.fromStream(inputSource,
+ new ComponentDescriptionState());
+
+ return readState.toDescription();
+ }
+
+ /*
+ * Create all the pragmas the reader needs to function
+ */
+ private static void setupReaderPragmas() {
+ reader.addPragma("name", buildStringCollapser("name", (name, state) -> state.setName(name)));
+
+ reader.addPragma("author", buildStringCollapser("author", (author, state) -> state.setAuthor(author)));
+
+ reader.addPragma("description", buildStringCollapser("description",
+ (description, state) -> state.setDescription(description)));
+
+ reader.addPragma("version", buildInteger("version", (version, state) -> state.setVersion(version)));
+ }
+}
diff --git a/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java b/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java
new file mode 100644
index 0000000..8d66f85
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/ComponentDescriptionState.java
@@ -0,0 +1,144 @@
+package bjc.utils.components;
+
+/**
+ * Internal state of component description parser
+ *
+ * @author ben
+ *
+ */
+public class ComponentDescriptionState {
+ // Tentative name of this component
+ private String name;
+
+ // Tentative description of this componet
+ private String description;
+
+ // Tentative author of this component
+ private String author;
+
+ // Tentative version of this component
+ private int version;
+
+ /**
+ * Set the author of this component
+ *
+ * @param author
+ * The author of this component
+ */
+ public void setAuthor(final String author) {
+ this.author = author;
+ }
+
+ /**
+ * Set the description of this component
+ *
+ * @param description
+ * The description of this component
+ */
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ /**
+ * Set the name of this component
+ *
+ * @param name
+ * The name of this component
+ */
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ /**
+ * Set the version of this component
+ *
+ * @param version
+ * The version of this component
+ */
+ public void setVersion(final int version) {
+ this.version = version;
+ }
+
+ /**
+ * Convert this state into the description it represents
+ *
+ * @return The description represented by this state
+ */
+ public ComponentDescription toDescription() {
+ return new ComponentDescription(name, author, description, version);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (author == null ? 0 : author.hashCode());
+ result = prime * result + (description == null ? 0 : description.hashCode());
+ result = prime * result + (name == null ? 0 : name.hashCode());
+ result = prime * result + version;
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ final ComponentDescriptionState other = (ComponentDescriptionState) obj;
+
+ if (author == null) {
+ if (other.author != null) return false;
+ } else if (!author.equals(other.author)) return false;
+
+ if (description == null) {
+ if (other.description != null) return false;
+ } else if (!description.equals(other.description)) return false;
+
+ if (name == null) {
+ if (other.name != null) return false;
+ } else if (!name.equals(other.name)) return false;
+
+ if (version != other.version) return false;
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("ComponentDescriptionState [");
+
+ if (name != null) {
+ builder.append("name=");
+ builder.append(name);
+ builder.append(", ");
+ }
+
+ if (description != null) {
+ builder.append("description=");
+ builder.append(description);
+ builder.append(", ");
+ }
+
+ if (author != null) {
+ builder.append("author=");
+ builder.append(author);
+ builder.append(", ");
+ }
+
+ builder.append("version=");
+ builder.append(version);
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/components/FileComponentRepository.java b/base/src/main/java/bjc/utils/components/FileComponentRepository.java
new file mode 100644
index 0000000..efde5c7
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/FileComponentRepository.java
@@ -0,0 +1,181 @@
+package bjc.utils.components;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Identity;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+import bjc.utils.funcutils.FileUtils;
+
+/**
+ * A component repository that loads its components from files in a directory
+ *
+ * @author ben
+ *
+ * @param <ComponentType>
+ * The type of component being read in
+ */
+public class FileComponentRepository<ComponentType extends IDescribedComponent>
+ implements IComponentRepository<ComponentType> {
+ // The logger to use for storing data about this class
+ private static final Logger CLASS_LOGGER = Logger.getLogger("FileComponentRepository");
+
+ // The internal storage of components
+ private IMap<String, ComponentType> components;
+
+ // The path that all the components came from
+ private Path sourceDirectory;
+
+ /**
+ * Create a new component repository sourcing components from files in a
+ * directory
+ *
+ * An exception thrown during the loading of a component will only cause
+ * the loading of that component to fail, but a warning will be logged.
+ *
+ * @param directory
+ * The directory to read component files from
+ * @param componentReader
+ * The function to use to convert files to components
+ */
+ public FileComponentRepository(final File directory,
+ final Function<File, ? extends ComponentType> componentReader) {
+ // Make sure we have valid arguments
+ if (directory == null)
+ throw new NullPointerException("Directory must not be null");
+ else if (!directory.isDirectory())
+ throw new IllegalArgumentException("File " + directory + " is not a directory.\n"
+ + "Components can only be read from a directory");
+ else if (componentReader == null) throw new NullPointerException("Component reader must not be null");
+
+ // Initialize our fields
+ components = new FunctionalMap<>();
+ sourceDirectory = directory.toPath().toAbsolutePath();
+
+ // Marker for making sure we don't skip the parent
+ final IHolder<Boolean> isFirstDir = new Identity<>(true);
+
+ // Predicate to use to traverse all the files in a directory,
+ // but
+ // not recurse into sub-directories
+ final BiPredicate<Path, BasicFileAttributes> firstLevelTraverser = (pth, attr) -> {
+ if (attr.isDirectory() && !isFirstDir.getValue()) /*
+ * Skip
+ * directories,
+ * they
+ * probably
+ * have
+ * component
+ * support
+ * files.
+ */
+ return false;
+
+ /*
+ * Don't skip the first directory, that's the parent
+ * directory
+ */
+ isFirstDir.replace(false);
+
+ return true;
+ };
+
+ // Try reading components
+ try {
+ FileUtils.traverseDirectory(sourceDirectory, firstLevelTraverser, (pth, attr) -> {
+ loadComponent(componentReader, pth);
+
+ // Keep loading components, even if this one
+ // failed
+ return true;
+ });
+ } catch (final IOException ioex) {
+ CLASS_LOGGER.log(Level.WARNING, ioex, () -> "Error found reading component from file.");
+ }
+ }
+
+ @Override
+ public IMap<String, ComponentType> getAll() {
+ return components;
+ }
+
+ @Override
+ public ComponentType getByName(final String name) {
+ return components.get(name);
+ }
+
+ @Override
+ public IList<ComponentType> getList() {
+ return components.valueList();
+ }
+
+ @Override
+ public String getSource() {
+ return "Components read from directory " + sourceDirectory + ".";
+ }
+
+ /*
+ * Load a component from a file
+ */
+ private void loadComponent(final Function<File, ? extends ComponentType> componentReader, final Path pth) {
+ try {
+ // Try to load the component
+ final ComponentType component = componentReader.apply(pth.toFile());
+
+ if (component == null)
+ throw new NullPointerException("Component reader read null component");
+ else if (!components.containsKey(component.getName())) {
+ // We only care about the latest version of a
+ // component
+ final ComponentType oldComponent = components.put(component.getName(), component);
+
+ if (oldComponent.getVersion() > component.getVersion()) {
+ components.put(oldComponent.getName(), oldComponent);
+ }
+ } else {
+ CLASS_LOGGER.warning("Found a duplicate component.\n"
+ + "Multiple versions of the same component are not currently supported.\n"
+ + "Only the latest version of the component" + component
+ + " will be registered .");
+ }
+ } catch (final Exception ex) {
+ CLASS_LOGGER.log(Level.WARNING, ex, () -> "Error found reading component from file "
+ + pth.toString() + ". This component will not be loaded");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("FileComponentRepository [");
+
+ if (components != null) {
+ builder.append("components=");
+ builder.append(components);
+ builder.append(", ");
+ }
+
+ if (sourceDirectory != null) {
+ builder.append("sourceDirectory=");
+ builder.append(sourceDirectory);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/components/IComponentRepository.java b/base/src/main/java/bjc/utils/components/IComponentRepository.java
new file mode 100644
index 0000000..6ee51f3
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/IComponentRepository.java
@@ -0,0 +1,49 @@
+package bjc.utils.components;
+
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A collection of implementations of a particular type of
+ * {@link IDescribedComponent}
+ *
+ * @author ben
+ *
+ * @param <ComponentType>
+ * The type of components contained in this repository
+ */
+public interface IComponentRepository<ComponentType extends IDescribedComponent> {
+ /**
+ * Get all of the components this repository knows about
+ *
+ * @return A map from component name to component, containing all of the
+ * components in the repositories
+ */
+ public IMap<String, ComponentType> getAll();
+
+ /**
+ * Get a component with a specific name
+ *
+ * @param name
+ * The name of the component to retrieve
+ * @return The named component, or null if no component with that name
+ * exists
+ */
+ public ComponentType getByName(String name);
+
+ /**
+ * Get a list of all the registered components
+ *
+ * @return A list of all the registered components
+ */
+ public default IList<ComponentType> getList() {
+ return getAll().valueList();
+ }
+
+ /**
+ * Get the source from which these components came
+ *
+ * @return The source from which these components came
+ */
+ public String getSource();
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/components/IDescribedComponent.java b/base/src/main/java/bjc/utils/components/IDescribedComponent.java
new file mode 100644
index 0000000..952b375
--- /dev/null
+++ b/base/src/main/java/bjc/utils/components/IDescribedComponent.java
@@ -0,0 +1,64 @@
+package bjc.utils.components;
+
+/**
+ * Represents a optional component that has status information associated with
+ * it
+ *
+ * @author ben
+ *
+ */
+public interface IDescribedComponent extends Comparable<IDescribedComponent> {
+ /**
+ * Get the author of this component
+ *
+ * Providing this is optional, with "Anonymous" as the default author
+ *
+ * @return The author of the component
+ */
+ default String getAuthor() {
+ return "Anonymous";
+ }
+
+ /**
+ * Get the description of this component
+ *
+ * Providing this is optional, with the default being a note that no
+ * description was provided
+ *
+ * @return The description of the component
+ */
+ default String getDescription() {
+ return "No description provided.";
+ }
+
+ /**
+ * Get the name of this component.
+ *
+ * This is the only thing required of all components
+ *
+ * @return The name of the component
+ */
+ String getName();
+
+ /**
+ * Get the version of this component
+ *
+ * Providing this is optional, with "1" as the default version
+ *
+ * @return The version of this component
+ */
+ default int getVersion() {
+ return 1;
+ }
+
+ @Override
+ default int compareTo(final IDescribedComponent o) {
+ int res = getName().compareTo(o.getName());
+
+ if (res == 0) {
+ res = getVersion() - o.getVersion();
+ }
+
+ return res;
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/BooleanToggle.java b/base/src/main/java/bjc/utils/data/BooleanToggle.java
new file mode 100644
index 0000000..12e3b2e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/BooleanToggle.java
@@ -0,0 +1,76 @@
+package bjc.utils.data;
+
+/**
+ * A simple {@link ValueToggle} that swaps between true and false.
+ *
+ * @author EVE
+ *
+ */
+public class BooleanToggle implements Toggle<Boolean> {
+ private boolean val;
+
+ /**
+ * Create a new, initially false, flip-flop.
+ */
+ public BooleanToggle() {
+ this(false);
+ }
+
+ /**
+ * Create a flip-flop with the specified initial value.
+ *
+ * @param initial
+ * The initial value of the flip-flop.
+ */
+ public BooleanToggle(final boolean initial) {
+ val = initial;
+ }
+
+ @Override
+ public Boolean get() {
+ final boolean res = val;
+
+ val = !res;
+
+ return res;
+ }
+
+ @Override
+ public Boolean peek() {
+ return val;
+ }
+
+ @Override
+ public void set(final boolean vl) {
+ val = vl;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+
+ result = prime * result + (val ? 1231 : 1237);
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof BooleanToggle)) return false;
+
+ final BooleanToggle other = (BooleanToggle) obj;
+
+ if (val != other.val) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("BooleanToggle [val=%s]", val);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/CircularIterator.java b/base/src/main/java/bjc/utils/data/CircularIterator.java
new file mode 100644
index 0000000..a708eba
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/CircularIterator.java
@@ -0,0 +1,81 @@
+package bjc.utils.data;
+
+import java.util.Iterator;
+
+/**
+ * An iterator that repeats elements from a provided iterable.
+ *
+ * @author EVE
+ *
+ * @param <E>
+ * The type of the iterable.
+ */
+public class CircularIterator<E> implements Iterator<E> {
+ /*
+ * The iterable, and our current iterator into it.
+ */
+ private Iterable<E> source;
+ private Iterator<E> curr;
+
+ /*
+ * Our current element.
+ */
+ private E curElm;
+
+ /*
+ * Should we actually get new iterators, or just repeat the last
+ * element?
+ */
+ private boolean doCircle;
+
+ /**
+ * Create a new circular iterator.
+ *
+ * @param src
+ * The iterable to iterate from.
+ *
+ * @param circ
+ * Should we actually do circular iteration, or just
+ * repeat the terminal element?
+ */
+ public CircularIterator(final Iterable<E> src, final boolean circ) {
+ source = src;
+ curr = source.iterator();
+
+ doCircle = circ;
+ }
+
+ /**
+ * Create a new circular iterator that does actual circular iteration.
+ *
+ * @param src
+ * The iterable to iterate from.
+ */
+ public CircularIterator(final Iterable<E> src) {
+ this(src, true);
+ }
+
+ @Override
+ public boolean hasNext() {
+ // We always have something
+ return true;
+ }
+
+ @Override
+ public E next() {
+ if (!curr.hasNext()) {
+ if (doCircle) {
+ curr = source.iterator();
+ } else return curElm;
+ }
+
+ curElm = curr.next();
+
+ return curElm;
+ }
+
+ @Override
+ public void remove() {
+ curr.remove();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/Either.java b/base/src/main/java/bjc/utils/data/Either.java
new file mode 100644
index 0000000..36b3324
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Either.java
@@ -0,0 +1,173 @@
+package bjc.utils.data;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Represents a pair where only one side has a value
+ *
+ * @author ben
+ * @param <LeftType>
+ * The type that could be on the left
+ * @param <RightType>
+ * The type that could be on the right
+ *
+ */
+public class Either<LeftType, RightType> implements IPair<LeftType, RightType> {
+ /**
+ * Create a new either with the left value occupied
+ *
+ * @param <LeftType>
+ * The type of the left value
+ * @param <RightType>
+ * The type of the empty right value
+ * @param left
+ * The value to put on the left
+ * @return An either with the left side occupied
+ */
+ public static <LeftType, RightType> Either<LeftType, RightType> left(final LeftType left) {
+ return new Either<>(left, null);
+ }
+
+ /**
+ * Create a new either with the right value occupied
+ *
+ * @param <LeftType>
+ * The type of the empty left value
+ * @param <RightType>
+ * The type of the right value
+ * @param right
+ * The value to put on the right
+ * @return An either with the right side occupied
+ */
+ public static <LeftType, RightType> Either<LeftType, RightType> right(final RightType right) {
+ return new Either<>(null, right);
+ }
+
+ private LeftType leftVal;
+
+ private RightType rightVal;
+
+ private boolean isLeft;
+
+ private Either(final LeftType left, final RightType right) {
+ if (left == null) {
+ rightVal = right;
+ } else {
+ leftVal = left;
+
+ isLeft = true;
+ }
+ }
+
+ @Override
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) {
+ if (binder == null) throw new NullPointerException("Binder must not be null");
+
+ return binder.apply(leftVal, rightVal);
+ }
+
+ @Override
+ public <BoundLeft> IPair<BoundLeft, RightType> bindLeft(
+ final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) {
+ if (leftBinder == null) throw new NullPointerException("Left binder must not be null");
+
+ if (isLeft) return leftBinder.apply(leftVal);
+
+ return new Either<>(null, rightVal);
+ }
+
+ @Override
+ public <BoundRight> IPair<LeftType, BoundRight> bindRight(
+ final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) {
+ if (rightBinder == null) throw new NullPointerException("Right binder must not be null");
+
+ if (isLeft) return new Either<>(leftVal, null);
+
+ return rightBinder.apply(rightVal);
+ }
+
+ @Override
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ final IPair<OtherLeft, OtherRight> otherPair,
+ final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner,
+ final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) {
+ if (otherPair == null)
+ throw new NullPointerException("Other pair must not be null");
+ else if (leftCombiner == null)
+ throw new NullPointerException("Left combiner must not be null");
+ else if (rightCombiner == null) throw new NullPointerException("Right combiner must not be null");
+
+ if (isLeft) return otherPair.bind((otherLeft, otherRight) -> {
+ return new Either<>(leftCombiner.apply(leftVal, otherLeft), null);
+ });
+
+ return otherPair.bind((otherLeft, otherRight) -> {
+ return new Either<>(null, rightCombiner.apply(rightVal, otherRight));
+ });
+ }
+
+ @Override
+ public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ if (isLeft) return new Either<>(mapper.apply(leftVal), null);
+
+ return new Either<>(null, rightVal);
+ }
+
+ @Override
+ public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ if (isLeft) return new Either<>(leftVal, null);
+
+ return new Either<>(null, mapper.apply(rightVal));
+ }
+
+ @Override
+ public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) {
+ if (merger == null) throw new NullPointerException("Merger must not be null");
+
+ return merger.apply(leftVal, rightVal);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (isLeft ? 1231 : 1237);
+ result = prime * result + (leftVal == null ? 0 : leftVal.hashCode());
+ result = prime * result + (rightVal == null ? 0 : rightVal.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Either<?, ?>)) return false;
+
+ final Either<?, ?> other = (Either<?, ?>) obj;
+
+ if (isLeft != other.isLeft) return false;
+
+ if (leftVal == null) {
+ if (other.leftVal != null) return false;
+ } else if (!leftVal.equals(other.leftVal)) return false;
+
+ if (rightVal == null) {
+ if (other.rightVal != null) return false;
+ } else if (!rightVal.equals(other.rightVal)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Either [leftVal='%s', rightVal='%s', isLeft=%s]", leftVal, rightVal, isLeft);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/GeneratingIterator.java b/base/src/main/java/bjc/utils/data/GeneratingIterator.java
new file mode 100644
index 0000000..9abca7c
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/GeneratingIterator.java
@@ -0,0 +1,53 @@
+package bjc.utils.data;
+
+import java.util.Iterator;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+/**
+ * An iterator that generates a series of elements from a single element.
+ *
+ * @author bjculkin
+ *
+ * @param <E>
+ * The type of element generated.
+ */
+public class GeneratingIterator<E> implements Iterator<E> {
+ private E state;
+
+ private UnaryOperator<E> transtion;
+
+ private Predicate<E> stpper;
+
+ /**
+ * Create a new generative iterator.
+ *
+ * @param initial
+ * The initial state of the generator.
+ *
+ * @param transition
+ * The function to apply to the state.
+ *
+ * @param stopper
+ * The predicate applied to the current state to
+ * determine when to stop.
+ */
+ public GeneratingIterator(E initial, UnaryOperator<E> transition, Predicate<E> stopper) {
+ state = initial;
+ transtion = transition;
+ stpper = stopper;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return stpper.test(state);
+ }
+
+ @Override
+ public E next() {
+ state = transtion.apply(state);
+
+ return state;
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/data/IHolder.java b/base/src/main/java/bjc/utils/data/IHolder.java
new file mode 100644
index 0000000..ca0b2ba
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/IHolder.java
@@ -0,0 +1,153 @@
+package bjc.utils.data;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.internals.BoundListHolder;
+import bjc.utils.data.internals.WrappedLazy;
+import bjc.utils.data.internals.WrappedOption;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.theory.Functor;
+
+/**
+ * A holder of a single value.
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type of value held
+ */
+public interface IHolder<ContainedType> extends Functor<ContainedType> {
+ /**
+ * Bind a function across the value in this container
+ *
+ * @param <BoundType>
+ * The type of value in this container
+ * @param binder
+ * The function to bind to the value
+ * @return A holder from binding the value
+ */
+ public <BoundType> IHolder<BoundType> bind(Function<ContainedType, IHolder<BoundType>> binder);
+
+ /**
+ * Apply an action to the value
+ *
+ * @param action
+ * The action to apply to the value
+ */
+ public default void doWith(final Consumer<? super ContainedType> action) {
+ transform(value -> {
+ action.accept(value);
+
+ return value;
+ });
+ }
+
+ @Override
+ default <ArgType, ReturnType> Function<Functor<ArgType>, Functor<ReturnType>> fmap(
+ final Function<ArgType, ReturnType> func) {
+ return argumentFunctor -> {
+ if (!(argumentFunctor instanceof IHolder<?>)) {
+ final String msg = "This functor only supports mapping over instances of IHolder";
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final IHolder<ArgType> holder = (IHolder<ArgType>) argumentFunctor;
+
+ return holder.map(func);
+ };
+ }
+
+ @Override
+ public default ContainedType getValue() {
+ return unwrap(value -> value);
+ }
+
+ /**
+ * Lifts a function to bind over this holder
+ *
+ * @param <NewType>
+ * The type of the functions return
+ * @param func
+ * The function to lift over the holder
+ * @return The function lifted over the holder
+ */
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(Function<ContainedType, NewType> func);
+
+ /**
+ * Make this holder lazy
+ *
+ * @return A lazy version of this holder
+ */
+ public default IHolder<ContainedType> makeLazy() {
+ return new WrappedLazy<>(this);
+ }
+
+ /**
+ * Make this holder a list
+ *
+ * @return A list version of this holder
+ */
+ public default IHolder<ContainedType> makeList() {
+ return new BoundListHolder<>(new FunctionalList<>(this));
+ }
+
+ /**
+ * Make this holder optional
+ *
+ * @return An optional version of this holder
+ */
+ public default IHolder<ContainedType> makeOptional() {
+ return new WrappedOption<>(this);
+ }
+
+ /**
+ * Create a new holder with a mapped version of the value in this
+ * holder.
+ *
+ * Does not change the internal state of this holder
+ *
+ * @param <MappedType>
+ * The type of the mapped value
+ * @param mapper
+ * The function to do mapping with
+ * @return A holder with the mapped value
+ */
+ public <MappedType> IHolder<MappedType> map(Function<ContainedType, MappedType> mapper);
+
+ /**
+ * Replace the held value with a new one
+ *
+ * @param newValue
+ * The value to hold instead
+ * @return The holder itself
+ */
+ public default IHolder<ContainedType> replace(final ContainedType newValue) {
+ return transform(oldValue -> {
+ return newValue;
+ });
+ }
+
+ /**
+ * Transform the value held in this holder
+ *
+ * @param transformer
+ * The function to transform the value with
+ * @return The holder itself, for easy chaining
+ */
+ public IHolder<ContainedType> transform(UnaryOperator<ContainedType> transformer);
+
+ /**
+ * Unwrap the value contained in this holder so that it is no longer
+ * held
+ *
+ * @param <UnwrappedType>
+ * The type of the unwrapped value
+ * @param unwrapper
+ * The function to use to unwrap the value
+ * @return The unwrapped held value
+ */
+ public <UnwrappedType> UnwrappedType unwrap(Function<ContainedType, UnwrappedType> unwrapper);
+}
diff --git a/base/src/main/java/bjc/utils/data/IPair.java b/base/src/main/java/bjc/utils/data/IPair.java
new file mode 100644
index 0000000..db8a1cb
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/IPair.java
@@ -0,0 +1,200 @@
+package bjc.utils.data;
+
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import bjc.utils.funcdata.theory.Bifunctor;
+
+/**
+ * Represents a pair of values
+ *
+ * @author ben
+ * @param <LeftType>
+ * The type of the left side of the pair
+ * @param <RightType>
+ * The type of the right side of the pair
+ *
+ */
+public interface IPair<LeftType, RightType> extends Bifunctor<LeftType, RightType> {
+ /**
+ * Bind a function across the values in this pair
+ *
+ * @param <BoundLeft>
+ * The type of the bound left
+ * @param <BoundRight>
+ * The type of the bound right
+ * @param binder
+ * The function to bind with
+ * @return The bound pair
+ */
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder);
+
+ /**
+ * Bind a function to the left value in this pair
+ *
+ * @param <BoundLeft>
+ * The type of the bound value
+ * @param leftBinder
+ * The function to use to bind
+ * @return A pair with the left type bound
+ */
+ public <BoundLeft> IPair<BoundLeft, RightType> bindLeft(
+ Function<LeftType, IPair<BoundLeft, RightType>> leftBinder);
+
+ /**
+ * Bind a function to the right value in this pair
+ *
+ * @param <BoundRight>
+ * The type of the bound value
+ * @param rightBinder
+ * The function to use to bind
+ * @return A pair with the right type bound
+ */
+ public <BoundRight> IPair<LeftType, BoundRight> bindRight(
+ Function<RightType, IPair<LeftType, BoundRight>> rightBinder);
+
+ /**
+ * Pairwise combine two pairs together
+ *
+ * @param <OtherLeft>
+ * The left type of the other pair
+ * @param <OtherRight>
+ * The right type of the other pair
+ * @param otherPair
+ * The pair to combine with
+ * @return The pairs, pairwise combined together
+ */
+ public default <OtherLeft, OtherRight> IPair<IPair<LeftType, OtherLeft>, IPair<RightType, OtherRight>> combine(
+ final IPair<OtherLeft, OtherRight> otherPair) {
+ return combine(otherPair, Pair<LeftType, OtherLeft>::new, Pair<RightType, OtherRight>::new);
+ }
+
+ /**
+ * Combine the contents of two pairs together
+ *
+ * @param <OtherLeft>
+ * The type of the left value of the other pair
+ * @param <OtherRight>
+ * The type of the right value of the other pair
+ * @param <CombinedLeft>
+ * The type of the left value of the combined pair
+ * @param <CombinedRight>
+ * The type of the right value of the combined pair
+ * @param otherPair
+ * The other pair to combine with
+ * @param leftCombiner
+ * @param rightCombiner
+ * @return A pair with its values combined
+ */
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ IPair<OtherLeft, OtherRight> otherPair,
+ BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner,
+ BiFunction<RightType, OtherRight, CombinedRight> rightCombiner);
+
+ /**
+ * Immediately perfom the specified action with the contents of this
+ * pair
+ *
+ * @param consumer
+ * The action to perform on the pair
+ */
+ public default void doWith(final BiConsumer<LeftType, RightType> consumer) {
+ merge((leftValue, rightValue) -> {
+ consumer.accept(leftValue, rightValue);
+
+ return null;
+ });
+ }
+
+ @Override
+ default <OldLeft, OldRight, NewLeft> LeftBifunctorMap<OldLeft, OldRight, NewLeft> fmapLeft(
+ final Function<OldLeft, NewLeft> func) {
+ return argumentPair -> {
+ if (!(argumentPair instanceof IPair<?, ?>)) {
+ final String msg = "This function can only be applied to instances of IPair";
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final IPair<OldLeft, OldRight> argPair = (IPair<OldLeft, OldRight>) argumentPair;
+
+ return argPair.mapLeft(func);
+ };
+ }
+
+ @Override
+ default <OldLeft, OldRight, NewRight> RightBifunctorMap<OldLeft, OldRight, NewRight>
+
+ fmapRight(final Function<OldRight, NewRight> func) {
+ return argumentPair -> {
+ if (!(argumentPair instanceof IPair<?, ?>)) {
+ final String msg = "This function can only be applied to instances of IPair";
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final IPair<OldLeft, OldRight> argPair = (IPair<OldLeft, OldRight>) argumentPair;
+
+ return argPair.mapRight(func);
+ };
+ }
+
+ /**
+ * Get the value on the left side of the pair
+ *
+ * @return The value on the left side of the pair
+ */
+ @Override
+ public default LeftType getLeft() {
+ return merge((leftValue, rightValue) -> leftValue);
+ }
+
+ /**
+ * Get the value on the right side of the pair
+ *
+ * @return The value on the right side of the pair
+ */
+ @Override
+ public default RightType getRight() {
+ return merge((leftValue, rightValue) -> rightValue);
+ }
+
+ /**
+ * Transform the value on the left side of the pair. Doesn't modify the
+ * pair
+ *
+ * @param <NewLeft>
+ * The new type of the left part of the pair
+ * @param mapper
+ * The function to use to transform the left part of the
+ * pair
+ * @return The pair, with its left part transformed
+ */
+ public <NewLeft> IPair<NewLeft, RightType> mapLeft(Function<LeftType, NewLeft> mapper);
+
+ /**
+ * Transform the value on the right side of the pair. Doesn't modify the
+ * pair
+ *
+ * @param <NewRight>
+ * The new type of the right part of the pair
+ * @param mapper
+ * The function to use to transform the right part of the
+ * pair
+ * @return The pair, with its right part transformed
+ */
+ public <NewRight> IPair<LeftType, NewRight> mapRight(Function<RightType, NewRight> mapper);
+
+ /**
+ * Merge the two values in this pair into a single value
+ *
+ * @param <MergedType>
+ * The type of the single value
+ * @param merger
+ * The function to use for merging
+ * @return The pair, merged into a single value
+ */
+ public <MergedType> MergedType merge(BiFunction<LeftType, RightType, MergedType> merger);
+}
diff --git a/base/src/main/java/bjc/utils/data/ITree.java b/base/src/main/java/bjc/utils/data/ITree.java
new file mode 100644
index 0000000..ff374e8
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/ITree.java
@@ -0,0 +1,234 @@
+package bjc.utils.data;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.funcdata.bst.TreeLinearizationMethod;
+import bjc.utils.functypes.ListFlattener;
+
+/**
+ * A node in a homogeneous tree with a unlimited amount of children.
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type of data contained in the tree nodes.
+ *
+ */
+public interface ITree<ContainedType> {
+ /**
+ * Append a child to this node.
+ *
+ * @param child
+ * The child to append to this node.
+ */
+ void addChild(ITree<ContainedType> child);
+
+ /**
+ * Prepend a child to this node.
+ *
+ * @param child
+ * The child to prepend to this node.
+ */
+ void prependChild(ITree<ContainedType> child);
+
+ /**
+ * Collapse a tree into a single version.
+ *
+ * @param <NewType>
+ * The intermediate type being folded.
+ *
+ * @param <ReturnedType>
+ * The type that is the end result.
+ *
+ * @param leafTransform
+ * The function to use to convert leaf values.
+ *
+ * @param nodeCollapser
+ * The function to use to convert internal nodes and
+ * their children.
+ *
+ * @param resultTransformer
+ * The function to use to convert a state to the returned
+ * version.
+ *
+ * @return The final transformed state.
+ */
+ <NewType, ReturnedType> ReturnedType collapse(Function<ContainedType, NewType> leafTransform,
+ Function<ContainedType, ListFlattener<NewType>> nodeCollapser,
+ Function<NewType, ReturnedType> resultTransformer);
+
+ /**
+ * Execute a given action for each of this tree's children.
+ *
+ * @param action
+ * The action to execute for each child.
+ */
+ void doForChildren(Consumer<ITree<ContainedType>> action);
+
+ /**
+ * Expand the nodes of a tree into trees, and then merge the contents of
+ * those trees into a single tree.
+ *
+ * @param mapper
+ * The function to use to map values into trees.
+ *
+ * @return A tree, with some nodes expanded into trees.
+ */
+ default ITree<ContainedType> flatMapTree(final Function<ContainedType, ITree<ContainedType>> mapper) {
+ return topDownTransform(dat -> TopDownTransformResult.PUSHDOWN, node -> {
+ if (node.getChildrenCount() > 0) {
+ final ITree<ContainedType> parent = node.transformHead(mapper);
+
+ node.doForChildren(parent::addChild);
+
+ return parent;
+ }
+
+ return node.transformHead(mapper);
+ });
+ }
+
+ /**
+ * Get the specified child of this tree.
+ *
+ * @param childNo
+ * The number of the child to get.
+ *
+ * @return The specified child of this tree.
+ */
+ default ITree<ContainedType> getChild(final int childNo) {
+ return transformChild(childNo, child -> child);
+ }
+
+ /**
+ * Get a count of the number of direct children this node has.
+ *
+ * @return The number of direct children this node has.
+ */
+ int getChildrenCount();
+
+ /**
+ * Get the data stored in this node.
+ *
+ * @return The data stored in this node.
+ */
+ default ContainedType getHead() {
+ return transformHead(head -> head);
+ }
+
+ /**
+ * Rebuild the tree with the same structure, but different nodes.
+ *
+ * @param <MappedType>
+ * The type of the new tree.
+ *
+ * @param leafTransformer
+ * The function to use to transform leaf tokens.
+ *
+ * @param operatorTransformer
+ * The function to use to transform internal tokens.
+ *
+ * @return The tree, with the nodes changed.
+ */
+ <MappedType> ITree<MappedType> rebuildTree(Function<ContainedType, MappedType> leafTransformer,
+ Function<ContainedType, MappedType> operatorTransformer);
+
+ /**
+ * Transform some of the nodes in this tree.
+ *
+ * @param nodePicker
+ * The predicate to use to pick nodes to transform.
+ *
+ * @param transformer
+ * The function to use to transform picked nodes.
+ */
+ void selectiveTransform(Predicate<ContainedType> nodePicker, UnaryOperator<ContainedType> transformer);
+
+ /**
+ * Do a top-down transform of the tree.
+ *
+ * @param transformPicker
+ * The function to use to pick how to progress.
+ *
+ * @param transformer
+ * The function used to transform picked subtrees.
+ *
+ * @return The tree with the transform applied to picked subtrees.
+ */
+ ITree<ContainedType> topDownTransform(Function<ContainedType, TopDownTransformResult> transformPicker,
+ UnaryOperator<ITree<ContainedType>> transformer);
+
+ /**
+ * Transform one of this nodes children.
+ *
+ * @param <TransformedType>
+ * The type of the transformed value.
+ *
+ * @param childNo
+ * The number of the child to transform.
+ *
+ * @param transformer
+ * The function to use to transform the value.
+ *
+ * @return The transformed value.
+ *
+ * @throws IllegalArgumentException
+ * if the childNo is out of bounds (0 <= childNo <=
+ * childCount()).
+ */
+ <TransformedType> TransformedType transformChild(int childNo,
+ Function<ITree<ContainedType>, TransformedType> transformer);
+
+ /**
+ * Transform the value that is the head of this node.
+ *
+ * @param <TransformedType>
+ * The type of the transformed value.
+ *
+ * @param transformer
+ * The function to use to transform the value.
+ *
+ * @return The transformed value.
+ */
+ <TransformedType> TransformedType transformHead(Function<ContainedType, TransformedType> transformer);
+
+ /**
+ * Transform the tree into a tree with a different type of token.
+ *
+ * @param <MappedType>
+ * The type of the new tree.
+ *
+ * @param transformer
+ * The function to use to transform tokens.
+ *
+ * @return A tree with the token types transformed.
+ */
+ default <MappedType> ITree<MappedType> transformTree(final Function<ContainedType, MappedType> transformer) {
+ return rebuildTree(transformer, transformer);
+ }
+
+ /**
+ * Perform an action on each part of the tree.
+ *
+ * @param linearizationMethod
+ * The way to traverse the tree.
+ *
+ * @param action
+ * The action to perform on each tree node.
+ */
+ void traverse(TreeLinearizationMethod linearizationMethod, Consumer<ContainedType> action);
+
+ /**
+ * Find the farthest to right child that satisfies the given predicate.
+ *
+ * @param childPred
+ * The predicate to satisfy.
+ *
+ * @return The index of the right-most child that satisfies the
+ * predicate, or -1 if one doesn't exist.
+ */
+ int revFind(Predicate<ITree<ContainedType>> childPred);
+}
diff --git a/base/src/main/java/bjc/utils/data/Identity.java b/base/src/main/java/bjc/utils/data/Identity.java
new file mode 100644
index 0000000..a8c8d70
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Identity.java
@@ -0,0 +1,118 @@
+package bjc.utils.data;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * @author ben
+ *
+ * @param <ContainedType>
+ */
+/**
+ * Simple implementation of IHolder that has no hidden behavior
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type contained in the holder
+ */
+public class Identity<ContainedType> implements IHolder<ContainedType> {
+ private ContainedType heldValue;
+
+ /**
+ * Create a holder holding null
+ */
+ public Identity() {
+ heldValue = null;
+ }
+
+ /**
+ * Create a holder holding the specified value
+ *
+ * @param value
+ * The value to hold
+ */
+ public Identity(final ContainedType value) {
+ heldValue = value;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ return binder.apply(heldValue);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (heldValue == null ? 0 : heldValue.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Identity)) return false;
+
+ final Identity<?> other = (Identity<?>) obj;
+
+ if (heldValue == null) {
+ if (other.heldValue != null) return false;
+ } else if (!heldValue.equals(other.heldValue)) return false;
+
+ return true;
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return (val) -> {
+ return new Identity<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ return new Identity<>(mapper.apply(heldValue));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Identity [heldValue=%s]", heldValue);
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ heldValue = transformer.apply(heldValue);
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ return unwrapper.apply(heldValue);
+ }
+
+ /**
+ * Create a new identity container.
+ *
+ * @param val
+ * The contained value.
+ *
+ * @return A new identity container.
+ */
+ public static <ContainedType> Identity<ContainedType> id(final ContainedType val) {
+ return new Identity<>(val);
+ }
+
+ /**
+ * Create a new empty identity container.
+ *
+ * @return A new empty identity container.
+ */
+ public static <ContainedType> Identity<ContainedType> id() {
+ return new Identity<>();
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/Lazy.java b/base/src/main/java/bjc/utils/data/Lazy.java
new file mode 100644
index 0000000..ca41b62
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Lazy.java
@@ -0,0 +1,194 @@
+package bjc.utils.data;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.internals.BoundLazy;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A holder that holds a means to create a value, but doesn't actually compute
+ * the value until it's needed
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ */
+public class Lazy<ContainedType> implements IHolder<ContainedType> {
+ private Supplier<ContainedType> valueSupplier;
+
+ private IList<UnaryOperator<ContainedType>> actions = new FunctionalList<>();
+
+ private boolean valueMaterialized;
+
+ private ContainedType heldValue;
+
+ /**
+ * Create a new lazy value from the specified seed value
+ *
+ * @param value
+ * The seed value to use
+ */
+ public Lazy(final ContainedType value) {
+ heldValue = value;
+
+ valueMaterialized = true;
+ }
+
+ /**
+ * Create a new lazy value from the specified value source
+ *
+ * @param supp
+ * The source of a value to use
+ */
+ public Lazy(final Supplier<ContainedType> supp) {
+ valueSupplier = new SingleSupplier<>(supp);
+
+ valueMaterialized = false;
+ }
+
+ private Lazy(final Supplier<ContainedType> supp, final IList<UnaryOperator<ContainedType>> pendingActions) {
+ valueSupplier = supp;
+
+ actions = pendingActions;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ final IList<UnaryOperator<ContainedType>> pendingActions = new FunctionalList<>();
+
+ actions.forEach(pendingActions::add);
+
+ final Supplier<ContainedType> supplier = () -> {
+ if (valueMaterialized) return heldValue;
+
+ return valueSupplier.get();
+ };
+
+ return new BoundLazy<>(() -> {
+ return new Lazy<>(supplier, pendingActions);
+ }, binder);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return val -> {
+ return new Lazy<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ final IList<UnaryOperator<ContainedType>> pendingActions = new FunctionalList<>();
+
+ actions.forEach(pendingActions::add);
+
+ return new Lazy<>(() -> {
+ ContainedType currVal = heldValue;
+
+ if (!valueMaterialized) {
+ currVal = valueSupplier.get();
+ }
+
+ return pendingActions.reduceAux(currVal, UnaryOperator<ContainedType>::apply,
+ value -> mapper.apply(value));
+ });
+ }
+
+ @Override
+ public String toString() {
+ if (valueMaterialized) {
+ if (actions.isEmpty())
+ return String.format("value[v='%s']", heldValue);
+ else return String.format("value[v='%s'] (has pending transforms)", heldValue);
+ }
+
+ return "(unmaterialized)";
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ actions.add(transformer);
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ if (!valueMaterialized) {
+ heldValue = valueSupplier.get();
+
+ valueMaterialized = true;
+ }
+
+ actions.forEach(action -> {
+ heldValue = action.apply(heldValue);
+ });
+
+ actions = new FunctionalList<>();
+
+ return unwrapper.apply(heldValue);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (actions == null ? 0 : actions.hashCode());
+ result = prime * result + (heldValue == null ? 0 : heldValue.hashCode());
+ result = prime * result + (valueMaterialized ? 1231 : 1237);
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Lazy<?>)) return false;
+
+ final Lazy<?> other = (Lazy<?>) obj;
+
+ if (valueMaterialized != other.valueMaterialized) return false;
+
+ if (valueMaterialized) {
+ if (heldValue == null) {
+ if (other.heldValue != null) return false;
+ } else if (!heldValue.equals(other.heldValue)) return false;
+ } else return false;
+
+ if (actions == null) {
+ if (other.actions != null) return false;
+ } else if (actions.getSize() > 0 || other.actions.getSize() > 0) return false;
+
+ return true;
+ }
+
+ /**
+ * Create a new lazy container with an already present value.
+ *
+ * @param val
+ * The value for the lazy container.
+ *
+ * @return A new lazy container holding that value.
+ */
+ public static <ContainedType> Lazy<ContainedType> lazy(final ContainedType val) {
+ return new Lazy<>(val);
+ }
+
+ /**
+ * Create a new lazy container with a suspended value.
+ *
+ * @param supp
+ * The suspended value for the lazy container.
+ *
+ * @return A new lazy container that will un-suspend the value when
+ * necessary.
+ */
+ public static <ContainedType> Lazy<ContainedType> lazy(final Supplier<ContainedType> supp) {
+ return new Lazy<>(supp);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/LazyPair.java b/base/src/main/java/bjc/utils/data/LazyPair.java
new file mode 100644
index 0000000..5cb85f3
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/LazyPair.java
@@ -0,0 +1,240 @@
+package bjc.utils.data;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import bjc.utils.data.internals.BoundLazyPair;
+import bjc.utils.data.internals.HalfBoundLazyPair;
+
+/**
+ * A lazy implementation of a pair
+ *
+ * @author ben
+ *
+ * @param <LeftType>
+ * The type on the left side of the pair
+ * @param <RightType>
+ * The type on the right side of the pair
+ *
+ */
+public class LazyPair<LeftType, RightType> implements IPair<LeftType, RightType> {
+ private LeftType leftValue;
+ private RightType rightValue;
+
+ private Supplier<LeftType> leftSupplier;
+ private Supplier<RightType> rightSupplier;
+
+ private boolean leftMaterialized;
+ private boolean rightMaterialized;
+
+ /**
+ * Create a new lazy pair, using the set values
+ *
+ * @param leftVal
+ * The value for the left side of the pair
+ * @param rightVal
+ * The value for the right side of the pair
+ */
+ public LazyPair(final LeftType leftVal, final RightType rightVal) {
+ leftValue = leftVal;
+ rightValue = rightVal;
+
+ leftMaterialized = true;
+ rightMaterialized = true;
+ }
+
+ /**
+ * Create a new lazy pair from the given value sources
+ *
+ * @param leftSupp
+ * The source for a value on the left side of the pair
+ * @param rightSupp
+ * The source for a value on the right side of the pair
+ */
+ public LazyPair(final Supplier<LeftType> leftSupp, final Supplier<RightType> rightSupp) {
+ // Use single suppliers to catch double-instantiation bugs
+ leftSupplier = new SingleSupplier<>(leftSupp);
+ rightSupplier = new SingleSupplier<>(rightSupp);
+
+ leftMaterialized = false;
+ rightMaterialized = false;
+ }
+
+ @Override
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) {
+ return new BoundLazyPair<>(leftSupplier, rightSupplier, binder);
+ }
+
+ @Override
+ public <BoundLeft> IPair<BoundLeft, RightType> bindLeft(
+ final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) {
+ final Supplier<LeftType> leftSupp = () -> {
+ if (leftMaterialized) return leftValue;
+
+ return leftSupplier.get();
+ };
+
+ return new HalfBoundLazyPair<>(leftSupp, leftBinder);
+ }
+
+ @Override
+ public <BoundRight> IPair<LeftType, BoundRight> bindRight(
+ final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) {
+ final Supplier<RightType> rightSupp = () -> {
+ if (rightMaterialized) return rightValue;
+
+ return rightSupplier.get();
+ };
+
+ return new HalfBoundLazyPair<>(rightSupp, rightBinder);
+ }
+
+ @Override
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ final IPair<OtherLeft, OtherRight> otherPair,
+ final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner,
+ final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) {
+ return otherPair.bind((otherLeft, otherRight) -> {
+ return bind((leftVal, rightVal) -> {
+ final CombinedLeft left = leftCombiner.apply(leftVal, otherLeft);
+ final CombinedRight right = rightCombiner.apply(rightVal, otherRight);
+
+ return new LazyPair<>(left, right);
+ });
+ });
+ }
+
+ @Override
+ public LeftType getLeft() {
+ if (!leftMaterialized) {
+ leftValue = leftSupplier.get();
+
+ leftMaterialized = true;
+ }
+
+ return leftValue;
+ }
+
+ @Override
+ public RightType getRight() {
+ if (!rightMaterialized) {
+ rightValue = rightSupplier.get();
+
+ rightMaterialized = true;
+ }
+
+ return rightValue;
+ }
+
+ @Override
+ public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) {
+ final Supplier<NewLeft> leftSupp = () -> {
+ if (leftMaterialized) return mapper.apply(leftValue);
+
+ return mapper.apply(leftSupplier.get());
+ };
+
+ final Supplier<RightType> rightSupp = () -> {
+ if (rightMaterialized) return rightValue;
+
+ return rightSupplier.get();
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) {
+ final Supplier<LeftType> leftSupp = () -> {
+ if (leftMaterialized) return leftValue;
+
+ return leftSupplier.get();
+ };
+
+ final Supplier<NewRight> rightSupp = () -> {
+ if (rightMaterialized) return mapper.apply(rightValue);
+
+ return mapper.apply(rightSupplier.get());
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) {
+ if (!leftMaterialized) {
+ leftValue = leftSupplier.get();
+
+ leftMaterialized = true;
+ }
+
+ if (!rightMaterialized) {
+ rightValue = rightSupplier.get();
+
+ rightMaterialized = true;
+ }
+
+ return merger.apply(leftValue, rightValue);
+ }
+
+ @Override
+ public String toString() {
+ String leftVal;
+ String rightVal;
+
+ if (leftMaterialized) {
+ leftVal = leftValue.toString();
+ } else {
+ leftVal = "(un-materialized)";
+ }
+
+ if (rightMaterialized) {
+ rightVal = rightValue.toString();
+ } else {
+ rightVal = "(un-materialized)";
+ }
+
+ return String.format("pair[l=%s,r=%s]", leftVal, rightVal);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (leftMaterialized ? 1231 : 1237);
+ result = prime * result + (leftValue == null ? 0 : leftValue.hashCode());
+ result = prime * result + (rightMaterialized ? 1231 : 1237);
+ result = prime * result + (rightValue == null ? 0 : rightValue.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof LazyPair<?, ?>)) return false;
+
+ final LazyPair<?, ?> other = (LazyPair<?, ?>) obj;
+
+ if (leftMaterialized != other.leftMaterialized) return false;
+
+ if (leftMaterialized) {
+ if (leftValue == null) {
+ if (other.leftValue != null) return false;
+ } else if (!leftValue.equals(other.leftValue)) return false;
+ } else return false;
+
+ if (rightMaterialized != other.rightMaterialized) return false;
+ if (rightMaterialized) {
+ if (rightValue == null) {
+ if (other.rightValue != null) return false;
+ } else if (!rightValue.equals(other.rightValue)) return false;
+ } else return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/ListHolder.java b/base/src/main/java/bjc/utils/data/ListHolder.java
new file mode 100644
index 0000000..142057c
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/ListHolder.java
@@ -0,0 +1,104 @@
+package bjc.utils.data;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.internals.BoundListHolder;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A holder that represents a set of non-deterministic computations
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type of contained value
+ */
+public class ListHolder<ContainedType> implements IHolder<ContainedType> {
+ private IList<ContainedType> heldValues;
+
+ /**
+ * Create a new list holder
+ *
+ * @param values
+ * The possible values for the computation
+ */
+ @SafeVarargs
+ public ListHolder(final ContainedType... values) {
+ heldValues = new FunctionalList<>();
+
+ if (values != null) {
+ for (final ContainedType containedValue : values) {
+ heldValues.add(containedValue);
+ }
+ }
+ }
+
+ private ListHolder(final IList<ContainedType> toHold) {
+ heldValues = toHold;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ final IList<IHolder<BoundType>> boundValues = heldValues.map(binder);
+
+ return new BoundListHolder<>(boundValues);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return val -> {
+ return new ListHolder<>(new FunctionalList<>(func.apply(val)));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ final IList<MappedType> mappedValues = heldValues.map(mapper);
+
+ return new ListHolder<>(mappedValues);
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ heldValues = heldValues.map(transformer);
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ return unwrapper.apply(heldValues.randItem());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ListHolder [heldValues=%s]", heldValues);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (heldValues == null ? 0 : heldValues.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof ListHolder<?>)) return false;
+
+ final ListHolder<?> other = (ListHolder<?>) obj;
+
+ if (heldValues == null) {
+ if (other.heldValues != null) return false;
+ } else if (!heldValues.equals(other.heldValues)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/Option.java b/base/src/main/java/bjc/utils/data/Option.java
new file mode 100644
index 0000000..37e0cde
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Option.java
@@ -0,0 +1,93 @@
+package bjc.utils.data;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * A holder that may or may not contain a value
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type of the value that may or may not be held
+ */
+public class Option<ContainedType> implements IHolder<ContainedType> {
+ private ContainedType held;
+
+ /**
+ * Create a new optional, using the given initial value
+ *
+ * @param seed
+ * The initial value for the optional
+ */
+ public Option(final ContainedType seed) {
+ held = seed;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ if (held == null) return new Option<>(null);
+
+ return binder.apply(held);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return val -> {
+ return new Option<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ if (held == null) return new Option<>(null);
+
+ return new Option<>(mapper.apply(held));
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ if (held != null) {
+ held = transformer.apply(held);
+ }
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ if (held == null) return null;
+
+ return unwrapper.apply(held);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Option [held='%s']", held);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+ result = prime * result + (held == null ? 0 : held.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Option<?>)) return false;
+
+ final Option<?> other = (Option<?>) obj;
+
+ if (held == null) {
+ if (other.held != null) return false;
+ } else if (!held.equals(other.held)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/Pair.java b/base/src/main/java/bjc/utils/data/Pair.java
new file mode 100644
index 0000000..e6796ba
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Pair.java
@@ -0,0 +1,135 @@
+package bjc.utils.data;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * A pair of values, with nothing special about them.
+ *
+ * @author ben
+ *
+ * @param <LeftType>
+ * The type of the left value
+ * @param <RightType>
+ * The type of the right value
+ */
+public class Pair<LeftType, RightType> implements IPair<LeftType, RightType> {
+ // The left value
+ private LeftType leftValue;
+
+ // The right value
+ private RightType rightValue;
+
+ /**
+ * Create a new pair with both sides set to null
+ */
+ public Pair() {
+
+ }
+
+ /**
+ * Create a new pair with both sides set to the specified values
+ *
+ * @param left
+ * The value of the left side
+ * @param right
+ * The value of the right side
+ */
+ public Pair(final LeftType left, final RightType right) {
+ leftValue = left;
+ rightValue = right;
+ }
+
+ @Override
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ final BiFunction<LeftType, RightType, IPair<BoundLeft, BoundRight>> binder) {
+ if (binder == null) throw new NullPointerException("Binder must not be null.");
+
+ return binder.apply(leftValue, rightValue);
+ }
+
+ @Override
+ public <BoundLeft> IPair<BoundLeft, RightType> bindLeft(
+ final Function<LeftType, IPair<BoundLeft, RightType>> leftBinder) {
+ if (leftBinder == null) throw new NullPointerException("Binder must not be null");
+
+ return leftBinder.apply(leftValue);
+ }
+
+ @Override
+ public <BoundRight> IPair<LeftType, BoundRight> bindRight(
+ final Function<RightType, IPair<LeftType, BoundRight>> rightBinder) {
+ if (rightBinder == null) throw new NullPointerException("Binder must not be null");
+
+ return rightBinder.apply(rightValue);
+ }
+
+ @Override
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ final IPair<OtherLeft, OtherRight> otherPair,
+ final BiFunction<LeftType, OtherLeft, CombinedLeft> leftCombiner,
+ final BiFunction<RightType, OtherRight, CombinedRight> rightCombiner) {
+ return otherPair.bind((otherLeft, otherRight) -> {
+ final CombinedLeft left = leftCombiner.apply(leftValue, otherLeft);
+ final CombinedRight right = rightCombiner.apply(rightValue, otherRight);
+
+ return new Pair<>(left, right);
+ });
+ }
+
+ @Override
+ public <NewLeft> IPair<NewLeft, RightType> mapLeft(final Function<LeftType, NewLeft> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ return new Pair<>(mapper.apply(leftValue), rightValue);
+ }
+
+ @Override
+ public <NewRight> IPair<LeftType, NewRight> mapRight(final Function<RightType, NewRight> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ return new Pair<>(leftValue, mapper.apply(rightValue));
+ }
+
+ @Override
+ public <MergedType> MergedType merge(final BiFunction<LeftType, RightType, MergedType> merger) {
+ if (merger == null) throw new NullPointerException("Merger must not be null");
+
+ return merger.apply(leftValue, rightValue);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Pair [leftValue='%s', rightValue='%s']", leftValue, rightValue);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (leftValue == null ? 0 : leftValue.hashCode());
+ result = prime * result + (rightValue == null ? 0 : rightValue.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Pair<?, ?>)) return false;
+
+ final Pair<?, ?> other = (Pair<?, ?>) obj;
+
+ if (leftValue == null) {
+ if (other.leftValue != null) return false;
+ } else if (!leftValue.equals(other.leftValue)) return false;
+
+ if (rightValue == null) {
+ if (other.rightValue != null) return false;
+ } else if (!rightValue.equals(other.rightValue)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/SingleIterator.java b/base/src/main/java/bjc/utils/data/SingleIterator.java
new file mode 100644
index 0000000..4069c3f
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/SingleIterator.java
@@ -0,0 +1,41 @@
+package bjc.utils.data;
+
+import java.util.Iterator;
+
+/**
+ * An iterator that will only ever yield one item.
+ *
+ * @author EVE
+ *
+ * @param <T>
+ * The type of the item.
+ */
+public class SingleIterator<T> implements Iterator<T> {
+ private final T itm;
+
+ private boolean yielded;
+
+ /**
+ * Create a iterator that yields a single item.
+ *
+ * @param item
+ * The item to yield.
+ */
+ public SingleIterator(final T item) {
+ itm = item;
+
+ yielded = false;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !yielded;
+ }
+
+ @Override
+ public T next() {
+ yielded = true;
+
+ return itm;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/SingleSupplier.java b/base/src/main/java/bjc/utils/data/SingleSupplier.java
new file mode 100644
index 0000000..c675ebf
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/SingleSupplier.java
@@ -0,0 +1,72 @@
+package bjc.utils.data;
+
+import java.util.function.Supplier;
+
+/**
+ * A supplier that can only supply one value.
+ *
+ * Attempting to retrieve another value will cause an exception to be thrown.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The supplied type
+ */
+public class SingleSupplier<T> implements Supplier<T> {
+ private static long nextID = 0;
+
+ private final Supplier<T> source;
+
+ private boolean gotten;
+
+ private final long id;
+
+ /*
+ * This is bad practice, but I want to know where the single
+ * instantiation was, in case of duplicate initiations.
+ */
+ private Exception instSite;
+
+ /**
+ * Create a new single supplier from an existing value
+ *
+ * @param supp
+ * The supplier to give a single value from
+ */
+ public SingleSupplier(final Supplier<T> supp) {
+ source = supp;
+
+ gotten = false;
+
+ id = nextID++;
+ }
+
+ @Override
+ public T get() {
+ if (gotten == true) {
+ final String msg = String.format(
+ "Attempted to retrieve value more than once from single supplier #%d", id);
+
+ final IllegalStateException isex = new IllegalStateException(msg);
+
+ isex.initCause(instSite);
+
+ throw isex;
+ }
+
+ gotten = true;
+
+ try {
+ throw new IllegalStateException("Previous instantiation here.");
+ } catch (final IllegalStateException isex) {
+ instSite = isex;
+ }
+
+ return source.get();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SingleSupplier [source='%s', gotten=%s, id=%s]", source, gotten, id);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/Toggle.java b/base/src/main/java/bjc/utils/data/Toggle.java
new file mode 100644
index 0000000..1e10dae
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Toggle.java
@@ -0,0 +1,35 @@
+package bjc.utils.data;
+
+/**
+ * A stateful holder that swaps between two values of the same type.
+ *
+ * @author EVE
+ *
+ * @param <E>
+ * The value stored in the toggle.
+ */
+public interface Toggle<E> {
+ /**
+ * Retrieve the currently-aligned value of this toggle, and swap the
+ * alignment.
+ *
+ * @return The previously-aligned value.
+ */
+ E get();
+
+ /**
+ * Retrieve the currently-aligned value without altering the alignment.
+ *
+ * @return The currently-aligned value.
+ */
+ E peek();
+
+ /**
+ * Change the alignment of the toggle.
+ *
+ * @param isLeft
+ * Whether the toggle should be left-aligned or not.
+ */
+ void set(boolean isLeft);
+
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java b/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java
new file mode 100644
index 0000000..1b87e52
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/TopDownTransformIterator.java
@@ -0,0 +1,208 @@
+package bjc.utils.data;
+
+import static bjc.utils.data.TopDownTransformResult.RTRANSFORM;
+
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.NoSuchElementException;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/*
+ * FIXME something is broken in here. fix it.
+ */
+public class TopDownTransformIterator<ContainedType> implements Iterator<ITree<ContainedType>> {
+ private final Function<ContainedType, TopDownTransformResult> picker;
+ private final BiFunction<ITree<ContainedType>, Consumer<Iterator<ITree<ContainedType>>>, ITree<ContainedType>> transform;
+
+ private ITree<ContainedType> preParent;
+ private ITree<ContainedType> postParent;
+
+ private final Deque<ITree<ContainedType>> preChildren;
+ private final Deque<ITree<ContainedType>> postChildren;
+
+ private TopDownTransformIterator<ContainedType> curChild;
+
+ private boolean done;
+ private boolean initial;
+
+ private final Deque<Iterator<ITree<ContainedType>>> toYield;
+ private Iterator<ITree<ContainedType>> curYield;
+
+ public TopDownTransformIterator(final Function<ContainedType, TopDownTransformResult> pickr,
+ final BiFunction<ITree<ContainedType>, Consumer<Iterator<ITree<ContainedType>>>, ITree<ContainedType>> transfrm,
+ final ITree<ContainedType> tree) {
+ preParent = tree;
+
+ preChildren = new LinkedList<>();
+ postChildren = new LinkedList<>();
+ toYield = new LinkedList<>();
+
+ picker = pickr;
+ transform = transfrm;
+
+ done = false;
+ initial = true;
+ }
+
+ public void addYield(final Iterator<ITree<ContainedType>> src) {
+ if (curYield != null) {
+ toYield.push(curYield);
+ }
+
+ curYield = src;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !done;
+ }
+
+ public ITree<ContainedType> flushYields(final ITree<ContainedType> val) {
+ if (curYield != null) {
+ toYield.add(new SingleIterator<>(val));
+
+ if (curYield.hasNext())
+ return curYield.next();
+ else {
+ while (toYield.size() != 0 && !curYield.hasNext()) {
+ curYield = toYield.pop();
+ }
+
+ if (toYield.size() == 0 && !curYield.hasNext()) {
+ curYield = null;
+ return val;
+ } else return curYield.next();
+ }
+ } else return val;
+ }
+
+ @Override
+ public ITree<ContainedType> next() {
+ if (done) throw new NoSuchElementException();
+
+ if (curYield != null) {
+ if (curYield.hasNext())
+ return curYield.next();
+ else {
+ while (toYield.size() != 0 && !curYield.hasNext()) {
+ curYield = toYield.pop();
+ }
+
+ if (toYield.size() == 0 && !curYield.hasNext()) {
+ curYield = null;
+ } else return curYield.next();
+ }
+ }
+
+ if (initial) {
+ final TopDownTransformResult res = picker.apply(preParent.getHead());
+
+ switch (res) {
+ case PASSTHROUGH:
+ postParent = new Tree<>(preParent.getHead());
+
+ if (preParent.getChildrenCount() != 0) {
+ for (int i = 0; i < preParent.getChildrenCount(); i++) {
+ preChildren.add(preParent.getChild(i));
+ }
+
+ // Return whatever the first child is
+ break;
+ } else {
+ done = true;
+ return flushYields(postParent);
+ }
+ case SKIP:
+ done = true;
+ return flushYields(preParent);
+ case TRANSFORM:
+ done = true;
+ return flushYields(transform.apply(preParent, this::addYield));
+ case RTRANSFORM:
+ preParent = transform.apply(preParent, this::addYield);
+ return flushYields(preParent);
+ case PUSHDOWN:
+ if (preParent.getChildrenCount() != 0) {
+ for (int i = 0; i < preParent.getChildrenCount(); i++) {
+ preChildren.add(preParent.getChild(i));
+ }
+
+ // Return whatever the first child is
+ break;
+ } else {
+ done = true;
+ return flushYields(transform.apply(new Tree<>(preParent.getHead()),
+ this::addYield));
+ }
+ case PULLUP:
+ final ITree<ContainedType> intRes = transform.apply(preParent, this::addYield);
+
+ postParent = new Tree<>(intRes.getHead());
+
+ if (intRes.getChildrenCount() != 0) {
+ for (int i = 0; i < intRes.getChildrenCount(); i++) {
+ preChildren.add(intRes.getChild(i));
+ }
+
+ // Return whatever the first child is
+ break;
+ } else {
+ done = true;
+ return flushYields(postParent);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown result type " + res);
+ }
+
+ if (res != RTRANSFORM) {
+ initial = false;
+ }
+ }
+
+ if (curChild == null || !curChild.hasNext()) {
+ if (preChildren.size() != 0) {
+ curChild = new TopDownTransformIterator<>(picker, transform, preChildren.pop());
+
+ final ITree<ContainedType> res = curChild.next();
+ System.out.println("\t\tTRACE: adding node " + res + " to children");
+ postChildren.add(res);
+
+ return flushYields(res);
+ } else {
+ ITree<ContainedType> res = null;
+
+ if (postParent == null) {
+ res = new Tree<>(preParent.getHead());
+
+ System.out.println("\t\tTRACE: adding nodes " + postChildren + " to " + res);
+
+ for (final ITree<ContainedType> child : postChildren) {
+ res.addChild(child);
+ }
+
+ // res = transform.apply(res,
+ // this::addYield);
+ } else {
+ res = postParent;
+
+ System.out.println("\t\tTRACE: adding nodes " + postChildren + " to " + res);
+ for (final ITree<ContainedType> child : postChildren) {
+ res.addChild(child);
+ }
+ }
+
+ done = true;
+ return flushYields(res);
+ }
+ } else {
+ final ITree<ContainedType> res = curChild.next();
+ System.out.println("\t\tTRACE: adding node " + res + " to children");
+ postChildren.add(res);
+
+ return flushYields(res);
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/TopDownTransformResult.java b/base/src/main/java/bjc/utils/data/TopDownTransformResult.java
new file mode 100644
index 0000000..ed41eae
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/TopDownTransformResult.java
@@ -0,0 +1,34 @@
+package bjc.utils.data;
+
+/**
+ * Represents the results for doing a top-down transform of a tree
+ *
+ * @author ben
+ *
+ */
+public enum TopDownTransformResult {
+ /**
+ * Do not do anything to this node, and ignore its children
+ */
+ SKIP,
+ /**
+ * Transform this node, and don't touch its children
+ */
+ TRANSFORM,
+ /**
+ * Transform this node, then do a top-down transform on the result
+ */
+ RTRANSFORM,
+ /**
+ * Ignore this node, and traverse its children
+ */
+ PASSTHROUGH,
+ /**
+ * Traverse the nodes of this children, then transform it
+ */
+ PUSHDOWN,
+ /**
+ * Transform this node, then traverse its children
+ */
+ PULLUP;
+}
diff --git a/base/src/main/java/bjc/utils/data/TransformIterator.java b/base/src/main/java/bjc/utils/data/TransformIterator.java
new file mode 100644
index 0000000..50f28b1
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/TransformIterator.java
@@ -0,0 +1,46 @@
+package bjc.utils.data;
+
+import java.util.Iterator;
+import java.util.function.Function;
+
+/**
+ * An iterator that transforms values from one type to another.
+ *
+ * @author EVE
+ *
+ * @param <S>
+ * The source iterator type.
+ *
+ * @param <D>
+ * The destination iterator type.
+ */
+public class TransformIterator<S, D> implements Iterator<D> {
+ private final Iterator<S> source;
+
+ private final Function<S, D> transform;
+
+ /**
+ * Create a new transform iterator.
+ *
+ * @param source
+ * The source iterator to use.
+ *
+ * @param transform
+ * The transform to apply.
+ */
+ public TransformIterator(final Iterator<S> source, final Function<S, D> transform) {
+ this.source = source;
+ this.transform = transform;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return source.hasNext();
+ }
+
+ @Override
+ public D next() {
+ return transform.apply(source.next());
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/data/Tree.java b/base/src/main/java/bjc/utils/data/Tree.java
new file mode 100644
index 0000000..a52f699
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/Tree.java
@@ -0,0 +1,390 @@
+package bjc.utils.data;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.bst.TreeLinearizationMethod;
+import bjc.utils.functypes.ListFlattener;
+
+/**
+ * A node in a homogeneous tree.
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ */
+public class Tree<ContainedType> implements ITree<ContainedType> {
+ private ContainedType data;
+
+ private IList<ITree<ContainedType>> children;
+ private boolean hasChildren;
+ private int childCount = 0;
+
+ private int ID;
+ private static int nextID = 0;
+
+ /**
+ * Create a new leaf node in a tree.
+ *
+ * @param leaf
+ * The data to store as a leaf node.
+ */
+ public Tree(final ContainedType leaf) {
+ data = leaf;
+
+ hasChildren = false;
+
+ ID = nextID++;
+ }
+
+ /**
+ * Create a new tree node with the specified children.
+ *
+ * @param leaf
+ * The data to hold in this node.
+ *
+ * @param childrn
+ * A list of children for this node.
+ */
+ public Tree(final ContainedType leaf, final IList<ITree<ContainedType>> childrn) {
+ this(leaf);
+
+ hasChildren = true;
+
+ childCount = childrn.getSize();
+
+ children = childrn;
+ }
+
+ /**
+ * Create a new tree node with the specified children.
+ *
+ * @param leaf
+ * The data to hold in this node.
+ *
+ * @param childrn
+ * A list of children for this node.
+ */
+ @SafeVarargs
+ public Tree(final ContainedType leaf, final ITree<ContainedType>... childrn) {
+ this(leaf);
+
+ hasChildren = true;
+
+ childCount = 0;
+
+ children = new FunctionalList<>();
+
+ for (final ITree<ContainedType> child : childrn) {
+ children.add(child);
+
+ childCount++;
+ }
+ }
+
+ @Override
+ public void addChild(final ITree<ContainedType> child) {
+ if (hasChildren == false) {
+ hasChildren = true;
+
+ children = new FunctionalList<>();
+ }
+
+ childCount++;
+
+ children.add(child);
+ }
+
+ @Override
+ public void prependChild(final ITree<ContainedType> child) {
+ if (hasChildren == false) {
+ hasChildren = true;
+
+ children = new FunctionalList<>();
+ }
+
+ childCount++;
+
+ children.prepend(child);
+ }
+
+ @Override
+ public void doForChildren(final Consumer<ITree<ContainedType>> action) {
+ if (childCount > 0) {
+ children.forEach(action);
+ }
+ }
+
+ @Override
+ public int getChildrenCount() {
+ return childCount;
+ }
+
+ @Override
+ public int revFind(final Predicate<ITree<ContainedType>> childPred) {
+ if (childCount == 0)
+ return -1;
+ else {
+ for (int i = childCount - 1; i >= 0; i--) {
+ if (childPred.test(getChild(i))) return i;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public void traverse(final TreeLinearizationMethod linearizationMethod, final Consumer<ContainedType> action) {
+ if (hasChildren) {
+ switch (linearizationMethod) {
+ case INORDER:
+ if (childCount != 2) {
+ final String msg = "Can only do in-order traversal for binary trees.";
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ children.getByIndex(0).traverse(linearizationMethod, action);
+
+ action.accept(data);
+
+ children.getByIndex(1).traverse(linearizationMethod, action);
+ break;
+ case POSTORDER:
+ children.forEach((child) -> child.traverse(linearizationMethod, action));
+
+ action.accept(data);
+ break;
+ case PREORDER:
+ action.accept(data);
+
+ children.forEach((child) -> child.traverse(linearizationMethod, action));
+ break;
+ default:
+ break;
+
+ }
+ } else {
+ action.accept(data);
+ }
+ }
+
+ @Override
+ public <NewType, ReturnedType> ReturnedType collapse(final Function<ContainedType, NewType> leafTransform,
+ final Function<ContainedType, ListFlattener<NewType>> nodeCollapser,
+ final Function<NewType, ReturnedType> resultTransformer) {
+ return resultTransformer.apply(internalCollapse(leafTransform, nodeCollapser));
+ }
+
+ @Override
+ public ITree<ContainedType> flatMapTree(final Function<ContainedType, ITree<ContainedType>> mapper) {
+ if (hasChildren) {
+ final ITree<ContainedType> flatMappedData = mapper.apply(data);
+
+ final IList<ITree<ContainedType>> mappedChildren = children
+ .map(child -> child.flatMapTree(mapper));
+
+ mappedChildren.forEach(child -> flatMappedData.addChild(child));
+
+ return flatMappedData;
+ }
+
+ return mapper.apply(data);
+ }
+
+ protected <NewType> NewType internalCollapse(final Function<ContainedType, NewType> leafTransform,
+ final Function<ContainedType, ListFlattener<NewType>> nodeCollapser) {
+ if (hasChildren) {
+ final Function<IList<NewType>, NewType> nodeTransformer = nodeCollapser.apply(data);
+
+ final IList<NewType> collapsedChildren = children.map(child -> {
+ final NewType collapsed = child.collapse(leafTransform, nodeCollapser,
+ subTreeVal -> subTreeVal);
+
+ return collapsed;
+ });
+
+ return nodeTransformer.apply(collapsedChildren);
+ }
+
+ return leafTransform.apply(data);
+ }
+
+ protected void internalToString(final StringBuilder builder, final int indentLevel, final boolean initial) {
+ for (int i = 0; i < indentLevel; i++) {
+ builder.append(">\t");
+ }
+
+ builder.append("Node #");
+ builder.append(ID);
+ builder.append(": ");
+ builder.append(data == null ? "(null)" : data.toString());
+ builder.append("\n");
+
+ if (hasChildren) {
+ children.forEach(child -> {
+ if (child instanceof Tree<?>) {
+ final Tree<ContainedType> kid = (Tree<ContainedType>) child;
+
+ kid.internalToString(builder, indentLevel + 1, false);
+ } else {
+ for (int i = 0; i < indentLevel + 1; i++) {
+ builder.append(">\t");
+ }
+
+ builder.append("Unknown node\n");
+ }
+ });
+ }
+ }
+
+ @Override
+ public <MappedType> ITree<MappedType> rebuildTree(final Function<ContainedType, MappedType> leafTransformer,
+ final Function<ContainedType, MappedType> operatorTransformer) {
+ if (hasChildren) {
+ final IList<ITree<MappedType>> mappedChildren = children.map(child -> {
+ return child.rebuildTree(leafTransformer, operatorTransformer);
+ });
+
+ return new Tree<>(operatorTransformer.apply(data), mappedChildren);
+ }
+
+ return new Tree<>(leafTransformer.apply(data));
+ }
+
+ @Override
+ public void selectiveTransform(final Predicate<ContainedType> nodePicker,
+ final UnaryOperator<ContainedType> transformer) {
+ if (hasChildren) {
+ children.forEach(child -> child.selectiveTransform(nodePicker, transformer));
+ } else {
+ data = transformer.apply(data);
+ }
+ }
+
+ @Override
+ public ITree<ContainedType> topDownTransform(
+ final Function<ContainedType, TopDownTransformResult> transformPicker,
+ final UnaryOperator<ITree<ContainedType>> transformer) {
+ final TopDownTransformResult transformResult = transformPicker.apply(data);
+
+ switch (transformResult) {
+ case PASSTHROUGH:
+ ITree<ContainedType> result = new Tree<>(data);
+
+ if (hasChildren) {
+ children.forEach(child -> {
+ final ITree<ContainedType> kid = child.topDownTransform(transformPicker,
+ transformer);
+
+ result.addChild(kid);
+ });
+ }
+
+ return result;
+ case SKIP:
+ return this;
+ case TRANSFORM:
+ return transformer.apply(this);
+ case RTRANSFORM:
+ return transformer.apply(this).topDownTransform(transformPicker, transformer);
+ case PUSHDOWN:
+ result = new Tree<>(data);
+
+ if (hasChildren) {
+ children.forEach(child -> {
+ final ITree<ContainedType> kid = child.topDownTransform(transformPicker,
+ transformer);
+
+ result.addChild(kid);
+ });
+ }
+
+ return transformer.apply(result);
+ case PULLUP:
+ final ITree<ContainedType> intermediateResult = transformer.apply(this);
+
+ result = new Tree<>(intermediateResult.getHead());
+
+ intermediateResult.doForChildren(child -> {
+ final ITree<ContainedType> kid = child.topDownTransform(transformPicker, transformer);
+
+ result.addChild(kid);
+ });
+
+ return result;
+ default:
+ final String msg = String.format("Recieved unknown transform result %s", transformResult);
+
+ throw new IllegalArgumentException(msg);
+ }
+ }
+
+ @Override
+ public <TransformedType> TransformedType transformChild(final int childNo,
+ final Function<ITree<ContainedType>, TransformedType> transformer) {
+ if (childNo < 0 || childNo > childCount - 1) {
+ final String msg = String.format("Child index #%d is invalid", childNo);
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final ITree<ContainedType> selectedKid = children.getByIndex(childNo);
+
+ return transformer.apply(selectedKid);
+ }
+
+ @Override
+ public <TransformedType> TransformedType transformHead(
+ final Function<ContainedType, TransformedType> transformer) {
+ return transformer.apply(data);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + childCount;
+ result = prime * result + (children == null ? 0 : children.hashCode());
+ result = prime * result + (data == null ? 0 : data.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ internalToString(builder, 1, true);
+
+ builder.deleteCharAt(builder.length() - 1);
+
+ return builder.toString();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Tree<?>)) return false;
+
+ final Tree<?> other = (Tree<?>) obj;
+
+ if (data == null) {
+ if (other.data != null) return false;
+ } else if (!data.equals(other.data)) return false;
+
+ if (childCount != other.childCount) return false;
+
+ if (children == null) {
+ if (other.children != null) return false;
+ } else if (!children.equals(other.children)) return false;
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/ValueToggle.java b/base/src/main/java/bjc/utils/data/ValueToggle.java
new file mode 100644
index 0000000..9193896
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/ValueToggle.java
@@ -0,0 +1,54 @@
+package bjc.utils.data;
+
+/**
+ * A simple implementation of {@link Toggle}.
+ *
+ * @author EVE
+ *
+ * @param <E>
+ * The type of value to toggle between.
+ */
+public class ValueToggle<E> implements Toggle<E> {
+ private final E lft;
+ private final E rght;
+
+ private final BooleanToggle alignment;
+
+ /**
+ * Create a new toggle.
+ *
+ * All toggles start right-aligned.
+ *
+ * @param left
+ * The value when the toggle is left-aligned.
+ *
+ * @param right
+ * The value when the toggle is right-aligned.
+ */
+ public ValueToggle(final E left, final E right) {
+ lft = left;
+
+ rght = right;
+
+ alignment = new BooleanToggle();
+ }
+
+ @Override
+ public E get() {
+ if (alignment.get())
+ return lft;
+ else return rght;
+ }
+
+ @Override
+ public E peek() {
+ if (alignment.peek())
+ return lft;
+ else return rght;
+ }
+
+ @Override
+ public void set(final boolean isLeft) {
+ alignment.set(isLeft);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/internals/BoundLazy.java b/base/src/main/java/bjc/utils/data/internals/BoundLazy.java
new file mode 100644
index 0000000..f71d32b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/BoundLazy.java
@@ -0,0 +1,145 @@
+package bjc.utils.data.internals;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Lazy;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/*
+ * Implements a lazy holder that has been bound
+ */
+public class BoundLazy<OldType, BoundContainedType> implements IHolder<BoundContainedType> {
+ /*
+ * The old value
+ */
+ private final Supplier<IHolder<OldType>> oldSupplier;
+
+ /*
+ * The function to use to transform the old value into a new value
+ */
+ private final Function<OldType, IHolder<BoundContainedType>> binder;
+
+ /*
+ * The bound value being held
+ */
+ private IHolder<BoundContainedType> boundHolder;
+
+ /*
+ * Whether the bound value has been actualized or not
+ */
+ private boolean holderBound;
+
+ /*
+ * Transformations currently pending on the bound value
+ */
+ private final IList<UnaryOperator<BoundContainedType>> actions = new FunctionalList<>();
+
+ /*
+ * Create a new bound lazy value
+ */
+ public BoundLazy(final Supplier<IHolder<OldType>> supp,
+ final Function<OldType, IHolder<BoundContainedType>> binder) {
+ oldSupplier = supp;
+ this.binder = binder;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<BoundContainedType, IHolder<BoundType>> bindr) {
+ if (bindr == null) throw new NullPointerException("Binder must not be null");
+
+ /*
+ * Prepare a list of pending actions
+ */
+ final IList<UnaryOperator<BoundContainedType>> pendingActions = new FunctionalList<>();
+ actions.forEach(pendingActions::add);
+
+ /*
+ * Create the new supplier of a value
+ */
+ final Supplier<IHolder<BoundContainedType>> typeSupplier = () -> {
+ IHolder<BoundContainedType> oldHolder = boundHolder;
+
+ /*
+ * Bind the value if it hasn't been bound before
+ */
+ if (!holderBound) {
+ oldHolder = oldSupplier.get().unwrap(binder);
+ }
+
+ /*
+ * Apply all the pending actions
+ */
+ return pendingActions.reduceAux(oldHolder, (action, state) -> {
+ return state.transform(action);
+ }, (value) -> value);
+ };
+
+ return new BoundLazy<>(typeSupplier, bindr);
+ }
+
+ @Override
+ public <NewType> Function<BoundContainedType, IHolder<NewType>> lift(
+ final Function<BoundContainedType, NewType> func) {
+ if (func == null) throw new NullPointerException("Function to lift must not be null");
+
+ return (val) -> {
+ return new Lazy<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<BoundContainedType, MappedType> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ // Prepare a list of pending actions
+ final IList<UnaryOperator<BoundContainedType>> pendingActions = new FunctionalList<>();
+ actions.forEach(pendingActions::add);
+
+ // Prepare the new supplier
+ final Supplier<MappedType> typeSupplier = () -> {
+ IHolder<BoundContainedType> oldHolder = boundHolder;
+
+ // Bound the value if it hasn't been bound
+ if (!holderBound) {
+ oldHolder = oldSupplier.get().unwrap(binder);
+ }
+
+ return pendingActions.reduceAux(oldHolder.getValue(), (action, state) -> {
+ return action.apply(state);
+ }, (value) -> mapper.apply(value));
+ };
+
+ return new Lazy<>(typeSupplier);
+ }
+
+ @Override
+ public String toString() {
+ if (holderBound) return boundHolder.toString();
+
+ return "(unmaterialized)";
+ }
+
+ @Override
+ public IHolder<BoundContainedType> transform(final UnaryOperator<BoundContainedType> transformer) {
+ if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ actions.add(transformer);
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<BoundContainedType, UnwrappedType> unwrapper) {
+ if (unwrapper == null) throw new NullPointerException("Unwrapper must not be null");
+
+ if (!holderBound) {
+ boundHolder = oldSupplier.get().unwrap(binder::apply);
+ }
+
+ return boundHolder.unwrap(unwrapper);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java b/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java
new file mode 100644
index 0000000..df6e60b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/BoundLazyPair.java
@@ -0,0 +1,199 @@
+package bjc.utils.data.internals;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.LazyPair;
+
+/*
+ * Implements a lazy pair that has been bound
+ */
+public class BoundLazyPair<OldLeft, OldRight, NewLeft, NewRight> implements IPair<NewLeft, NewRight> {
+ /*
+ * The supplier of the left value
+ */
+ private final Supplier<OldLeft> leftSupplier;
+ /*
+ * The supplier of the right value
+ */
+ private final Supplier<OldRight> rightSupplier;
+
+ /*
+ * The binder to transform values
+ */
+ private final BiFunction<OldLeft, OldRight, IPair<NewLeft, NewRight>> binder;
+
+ /*
+ * The bound pair
+ */
+ private IPair<NewLeft, NewRight> boundPair;
+
+ /*
+ * Whether the pair has been bound yet
+ */
+ private boolean pairBound;
+
+ public BoundLazyPair(final Supplier<OldLeft> leftSupp, final Supplier<OldRight> rightSupp,
+ final BiFunction<OldLeft, OldRight, IPair<NewLeft, NewRight>> bindr) {
+ leftSupplier = leftSupp;
+ rightSupplier = rightSupp;
+ binder = bindr;
+ }
+
+ @Override
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ final BiFunction<NewLeft, NewRight, IPair<BoundLeft, BoundRight>> bindr) {
+ if (bindr == null) throw new NullPointerException("Binder must not be null");
+
+ final IHolder<IPair<NewLeft, NewRight>> newPair = new Identity<>(boundPair);
+ final IHolder<Boolean> newPairMade = new Identity<>(pairBound);
+
+ final Supplier<NewLeft> leftSupp = () -> {
+ if (!newPairMade.getValue()) {
+ newPair.replace(binder.apply(leftSupplier.get(), rightSupplier.get()));
+
+ newPairMade.replace(true);
+ }
+
+ return newPair.unwrap((pair) -> pair.getLeft());
+ };
+
+ final Supplier<NewRight> rightSupp = () -> {
+ if (!newPairMade.getValue()) {
+ newPair.replace(binder.apply(leftSupplier.get(), rightSupplier.get()));
+
+ newPairMade.replace(true);
+ }
+
+ return newPair.unwrap((pair) -> pair.getRight());
+ };
+
+ return new BoundLazyPair<>(leftSupp, rightSupp, bindr);
+ }
+
+ @Override
+ public <BoundLeft> IPair<BoundLeft, NewRight> bindLeft(
+ final Function<NewLeft, IPair<BoundLeft, NewRight>> leftBinder) {
+ if (leftBinder == null) throw new NullPointerException("Left binder must not be null");
+
+ final Supplier<NewLeft> leftSupp = () -> {
+ IPair<NewLeft, NewRight> newPair = boundPair;
+
+ if (!pairBound) {
+ newPair = binder.apply(leftSupplier.get(), rightSupplier.get());
+ }
+
+ return newPair.getLeft();
+ };
+
+ return new HalfBoundLazyPair<>(leftSupp, leftBinder);
+ }
+
+ @Override
+ public <BoundRight> IPair<NewLeft, BoundRight> bindRight(
+ final Function<NewRight, IPair<NewLeft, BoundRight>> rightBinder) {
+ if (rightBinder == null) throw new NullPointerException("Right binder must not be null");
+
+ final Supplier<NewRight> rightSupp = () -> {
+ IPair<NewLeft, NewRight> newPair = boundPair;
+
+ if (!pairBound) {
+ newPair = binder.apply(leftSupplier.get(), rightSupplier.get());
+ }
+
+ return newPair.getRight();
+ };
+
+ return new HalfBoundLazyPair<>(rightSupp, rightBinder);
+ }
+
+ @Override
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ final IPair<OtherLeft, OtherRight> otherPair,
+ final BiFunction<NewLeft, OtherLeft, CombinedLeft> leftCombiner,
+ final BiFunction<NewRight, OtherRight, CombinedRight> rightCombiner) {
+ if (otherPair == null)
+ throw new NullPointerException("Other pair must not be null");
+ else if (leftCombiner == null)
+ throw new NullPointerException("Left combiner must not be null");
+ else if (rightCombiner == null) throw new NullPointerException("Right combiner must not be null");
+
+ return otherPair.bind((otherLeft, otherRight) -> {
+ return bind((leftVal, rightVal) -> {
+ return new LazyPair<>(leftCombiner.apply(leftVal, otherLeft),
+ rightCombiner.apply(rightVal, otherRight));
+ });
+ });
+ }
+
+ @Override
+ public <NewLeftType> IPair<NewLeftType, NewRight> mapLeft(final Function<NewLeft, NewLeftType> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ final Supplier<NewLeftType> leftSupp = () -> {
+ if (!pairBound) {
+ final NewLeft leftVal = binder.apply(leftSupplier.get(), rightSupplier.get()).getLeft();
+
+ return mapper.apply(leftVal);
+ }
+
+ return mapper.apply(boundPair.getLeft());
+ };
+
+ final Supplier<NewRight> rightSupp = () -> {
+ if (!pairBound) return binder.apply(leftSupplier.get(), rightSupplier.get()).getRight();
+
+ return boundPair.getRight();
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <NewRightType> IPair<NewLeft, NewRightType> mapRight(final Function<NewRight, NewRightType> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ final Supplier<NewLeft> leftSupp = () -> {
+ if (!pairBound) return binder.apply(leftSupplier.get(), rightSupplier.get()).getLeft();
+
+ return boundPair.getLeft();
+ };
+
+ final Supplier<NewRightType> rightSupp = () -> {
+ if (!pairBound) {
+ final NewRight rightVal = binder.apply(leftSupplier.get(), rightSupplier.get())
+ .getRight();
+
+ return mapper.apply(rightVal);
+ }
+
+ return mapper.apply(boundPair.getRight());
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <MergedType> MergedType merge(final BiFunction<NewLeft, NewRight, MergedType> merger) {
+ if (merger == null) throw new NullPointerException("Merger must not be null");
+
+ if (!pairBound) {
+ boundPair = binder.apply(leftSupplier.get(), rightSupplier.get());
+
+ pairBound = true;
+ }
+
+ return boundPair.merge(merger);
+ }
+
+ @Override
+ public String toString() {
+ if (pairBound) return boundPair.toString();
+
+ return "(un-materialized)";
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java b/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java
new file mode 100644
index 0000000..f3799fd
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/BoundListHolder.java
@@ -0,0 +1,68 @@
+package bjc.utils.data.internals;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.ListHolder;
+import bjc.utils.funcdata.IList;
+
+/*
+ * Holds a list, converted into a holder
+ */
+public class BoundListHolder<ContainedType> implements IHolder<ContainedType> {
+ private final IList<IHolder<ContainedType>> heldHolders;
+
+ public BoundListHolder(final IList<IHolder<ContainedType>> toHold) {
+ heldHolders = toHold;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ if (binder == null) throw new NullPointerException("Binder must not be null");
+
+ final IList<IHolder<BoundType>> boundHolders = heldHolders.map((containedHolder) -> {
+ return containedHolder.bind(binder);
+ });
+
+ return new BoundListHolder<>(boundHolders);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ if (func == null) throw new NullPointerException("Function to lift must not be null");
+
+ return (val) -> {
+ return new ListHolder<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ if (mapper == null) throw new NullPointerException("Mapper must not be null");
+
+ final IList<IHolder<MappedType>> mappedHolders = heldHolders.map((containedHolder) -> {
+ return containedHolder.map(mapper);
+ });
+
+ return new BoundListHolder<>(mappedHolders);
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ heldHolders.forEach((containedHolder) -> {
+ containedHolder.transform(transformer);
+ });
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ if (unwrapper == null) throw new NullPointerException("Unwrapper must not be null");
+
+ return heldHolders.randItem().unwrap(unwrapper);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java b/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java
new file mode 100644
index 0000000..8cac38b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/HalfBoundLazyPair.java
@@ -0,0 +1,149 @@
+package bjc.utils.data.internals;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.LazyPair;
+
+/*
+ * A lazy pair, with only one side bound
+ */
+public class HalfBoundLazyPair<OldType, NewLeft, NewRight> implements IPair<NewLeft, NewRight> {
+ private final Supplier<OldType> oldSupplier;
+
+ private final Function<OldType, IPair<NewLeft, NewRight>> binder;
+
+ private IPair<NewLeft, NewRight> boundPair;
+ private boolean pairBound;
+
+ public HalfBoundLazyPair(final Supplier<OldType> oldSupp,
+ final Function<OldType, IPair<NewLeft, NewRight>> bindr) {
+ oldSupplier = oldSupp;
+ binder = bindr;
+ }
+
+ @Override
+ public <BoundLeft, BoundRight> IPair<BoundLeft, BoundRight> bind(
+ final BiFunction<NewLeft, NewRight, IPair<BoundLeft, BoundRight>> bindr) {
+ final IHolder<IPair<NewLeft, NewRight>> newPair = new Identity<>(boundPair);
+ final IHolder<Boolean> newPairMade = new Identity<>(pairBound);
+
+ final Supplier<NewLeft> leftSupp = () -> {
+ if (!newPairMade.getValue()) {
+ newPair.replace(binder.apply(oldSupplier.get()));
+ newPairMade.replace(true);
+ }
+
+ return newPair.unwrap((pair) -> pair.getLeft());
+ };
+
+ final Supplier<NewRight> rightSupp = () -> {
+ if (!newPairMade.getValue()) {
+ newPair.replace(binder.apply(oldSupplier.get()));
+ newPairMade.replace(true);
+ }
+
+ return newPair.unwrap((pair) -> pair.getRight());
+ };
+
+ return new BoundLazyPair<>(leftSupp, rightSupp, bindr);
+ }
+
+ @Override
+ public <BoundLeft> IPair<BoundLeft, NewRight> bindLeft(
+ final Function<NewLeft, IPair<BoundLeft, NewRight>> leftBinder) {
+ final Supplier<NewLeft> leftSupp = () -> {
+ IPair<NewLeft, NewRight> newPair = boundPair;
+
+ if (!pairBound) {
+ newPair = binder.apply(oldSupplier.get());
+ }
+
+ return newPair.getLeft();
+ };
+
+ return new HalfBoundLazyPair<>(leftSupp, leftBinder);
+ }
+
+ @Override
+ public <BoundRight> IPair<NewLeft, BoundRight> bindRight(
+ final Function<NewRight, IPair<NewLeft, BoundRight>> rightBinder) {
+ final Supplier<NewRight> rightSupp = () -> {
+ IPair<NewLeft, NewRight> newPair = boundPair;
+
+ if (!pairBound) {
+ newPair = binder.apply(oldSupplier.get());
+ }
+
+ return newPair.getRight();
+ };
+
+ return new HalfBoundLazyPair<>(rightSupp, rightBinder);
+ }
+
+ @Override
+ public <OtherLeft, OtherRight, CombinedLeft, CombinedRight> IPair<CombinedLeft, CombinedRight> combine(
+ final IPair<OtherLeft, OtherRight> otherPair,
+ final BiFunction<NewLeft, OtherLeft, CombinedLeft> leftCombiner,
+ final BiFunction<NewRight, OtherRight, CombinedRight> rightCombiner) {
+ return otherPair.bind((otherLeft, otherRight) -> {
+ return bind((leftVal, rightVal) -> {
+ return new LazyPair<>(leftCombiner.apply(leftVal, otherLeft),
+ rightCombiner.apply(rightVal, otherRight));
+ });
+ });
+ }
+
+ @Override
+ public <NewLeftType> IPair<NewLeftType, NewRight> mapLeft(final Function<NewLeft, NewLeftType> mapper) {
+ final Supplier<NewLeftType> leftSupp = () -> {
+ if (pairBound) return mapper.apply(boundPair.getLeft());
+
+ final NewLeft leftVal = binder.apply(oldSupplier.get()).getLeft();
+
+ return mapper.apply(leftVal);
+ };
+
+ final Supplier<NewRight> rightSupp = () -> {
+ if (pairBound) return boundPair.getRight();
+
+ return binder.apply(oldSupplier.get()).getRight();
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <NewRightType> IPair<NewLeft, NewRightType> mapRight(final Function<NewRight, NewRightType> mapper) {
+ final Supplier<NewLeft> leftSupp = () -> {
+ if (pairBound) return boundPair.getLeft();
+
+ return binder.apply(oldSupplier.get()).getLeft();
+ };
+
+ final Supplier<NewRightType> rightSupp = () -> {
+ if (pairBound) return mapper.apply(boundPair.getRight());
+
+ final NewRight rightVal = binder.apply(oldSupplier.get()).getRight();
+
+ return mapper.apply(rightVal);
+ };
+
+ return new LazyPair<>(leftSupp, rightSupp);
+ }
+
+ @Override
+ public <MergedType> MergedType merge(final BiFunction<NewLeft, NewRight, MergedType> merger) {
+ if (!pairBound) {
+ boundPair = binder.apply(oldSupplier.get());
+
+ pairBound = true;
+ }
+
+ return boundPair.merge(merger);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java b/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java
new file mode 100644
index 0000000..4175724
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/WrappedLazy.java
@@ -0,0 +1,62 @@
+package bjc.utils.data.internals;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Lazy;
+
+public class WrappedLazy<ContainedType> implements IHolder<ContainedType> {
+ private final IHolder<IHolder<ContainedType>> held;
+
+ public WrappedLazy(final IHolder<ContainedType> wrappedHolder) {
+ held = new Lazy<>(wrappedHolder);
+ }
+
+ // This has an extra parameter, because otherwise it erases to the same
+ // as the public one
+ private WrappedLazy(final IHolder<IHolder<ContainedType>> wrappedHolder, final boolean dummy) {
+ held = wrappedHolder;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ final IHolder<IHolder<BoundType>> newHolder = held.map((containedHolder) -> {
+ return containedHolder.bind(binder);
+ });
+
+ return new WrappedLazy<>(newHolder, false);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return (val) -> {
+ return new Lazy<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ final IHolder<IHolder<MappedType>> newHolder = held.map((containedHolder) -> {
+ return containedHolder.map(mapper);
+ });
+
+ return new WrappedLazy<>(newHolder, false);
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ held.transform((containedHolder) -> {
+ return containedHolder.transform(transformer);
+ });
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ return held.unwrap((containedHolder) -> {
+ return containedHolder.unwrap(unwrapper);
+ });
+ }
+}
diff --git a/base/src/main/java/bjc/utils/data/internals/WrappedOption.java b/base/src/main/java/bjc/utils/data/internals/WrappedOption.java
new file mode 100644
index 0000000..512c699
--- /dev/null
+++ b/base/src/main/java/bjc/utils/data/internals/WrappedOption.java
@@ -0,0 +1,76 @@
+package bjc.utils.data.internals;
+
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Option;
+
+public class WrappedOption<ContainedType> implements IHolder<ContainedType> {
+ private final IHolder<IHolder<ContainedType>> held;
+
+ public WrappedOption(final IHolder<ContainedType> seedValue) {
+ held = new Option<>(seedValue);
+ }
+
+ private WrappedOption(final IHolder<IHolder<ContainedType>> toHold, final boolean dummy) {
+ held = toHold;
+ }
+
+ @Override
+ public <BoundType> IHolder<BoundType> bind(final Function<ContainedType, IHolder<BoundType>> binder) {
+ final IHolder<IHolder<BoundType>> newHolder = held.map((containedHolder) -> {
+ return containedHolder.bind((containedValue) -> {
+ if (containedValue == null) return new Option<>(null);
+
+ return binder.apply(containedValue);
+ });
+ });
+
+ return new WrappedOption<>(newHolder, false);
+ }
+
+ @Override
+ public <NewType> Function<ContainedType, IHolder<NewType>> lift(final Function<ContainedType, NewType> func) {
+ return (val) -> {
+ return new Option<>(func.apply(val));
+ };
+ }
+
+ @Override
+ public <MappedType> IHolder<MappedType> map(final Function<ContainedType, MappedType> mapper) {
+ final IHolder<IHolder<MappedType>> newHolder = held.map((containedHolder) -> {
+ return containedHolder.map((containedValue) -> {
+ if (containedValue == null) return null;
+
+ return mapper.apply(containedValue);
+ });
+ });
+
+ return new WrappedOption<>(newHolder, false);
+ }
+
+ @Override
+ public IHolder<ContainedType> transform(final UnaryOperator<ContainedType> transformer) {
+ held.transform((containedHolder) -> {
+ return containedHolder.transform((containedValue) -> {
+ if (containedValue == null) return null;
+
+ return transformer.apply(containedValue);
+ });
+ });
+
+ return this;
+ }
+
+ @Override
+ public <UnwrappedType> UnwrappedType unwrap(final Function<ContainedType, UnwrappedType> unwrapper) {
+ return held.unwrap((containedHolder) -> {
+ return containedHolder.unwrap((containedValue) -> {
+ if (containedValue == null) return null;
+
+ return unwrapper.apply(containedValue);
+ });
+ });
+ }
+}
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
diff --git a/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java b/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java
new file mode 100644
index 0000000..6f5a68a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/exceptions/FileNotChosenException.java
@@ -0,0 +1,31 @@
+package bjc.utils.exceptions;
+
+import java.io.IOException;
+
+/**
+ * Represents the user failing to choose a file.
+ *
+ * @author ben
+ *
+ */
+public class FileNotChosenException extends IOException {
+ // Version ID for serialization
+ private static final long serialVersionUID = -8753348705210831096L;
+
+ /**
+ * Create a new exception
+ */
+ public FileNotChosenException() {
+ super();
+ }
+
+ /**
+ * Create a new exception with the given cause
+ *
+ * @param cause
+ * The cause of why the exception was thrown
+ */
+ public FileNotChosenException(final String cause) {
+ super(cause);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java b/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java
new file mode 100644
index 0000000..1ad339d
--- /dev/null
+++ b/base/src/main/java/bjc/utils/exceptions/PragmaFormatException.java
@@ -0,0 +1,31 @@
+package bjc.utils.exceptions;
+
+import java.util.InputMismatchException;
+
+/**
+ * The exception to throw whenever a pragma is used with invalid syntax
+ *
+ * @author ben
+ *
+ */
+public class PragmaFormatException extends InputMismatchException {
+ // Version ID for serialization
+ private static final long serialVersionUID = 1288536477368021069L;
+
+ /**
+ * Create a new exception
+ */
+ public PragmaFormatException() {
+ super();
+ }
+
+ /**
+ * Create a new exception with the given message
+ *
+ * @param message
+ * The message to explain why the exception was thrown
+ */
+ public PragmaFormatException(final String message) {
+ super(message);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java b/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java
new file mode 100644
index 0000000..6fc9113
--- /dev/null
+++ b/base/src/main/java/bjc/utils/exceptions/UnknownPragmaException.java
@@ -0,0 +1,25 @@
+package bjc.utils.exceptions;
+
+import java.util.InputMismatchException;
+
+/**
+ * Represents a error from encountering a unknown pragma
+ *
+ * @author ben
+ *
+ */
+public class UnknownPragmaException extends InputMismatchException {
+ // Version ID for serialization
+ private static final long serialVersionUID = -4277573484926638662L;
+
+ /**
+ * Create a new exception with the given cause
+ *
+ * @param cause
+ * The cause for throwing this exception
+ */
+ public UnknownPragmaException(final String cause) {
+ super(cause);
+ }
+
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java b/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java
new file mode 100644
index 0000000..909c5e9
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/ExtendedMap.java
@@ -0,0 +1,127 @@
+package bjc.utils.funcdata;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import bjc.utils.funcutils.ListUtils;
+
+class ExtendedMap<KeyType, ValueType> implements IMap<KeyType, ValueType> {
+ private final IMap<KeyType, ValueType> delegate;
+
+ private final IMap<KeyType, ValueType> store;
+
+ public ExtendedMap(final IMap<KeyType, ValueType> delegate, final IMap<KeyType, ValueType> store) {
+ this.delegate = delegate;
+ this.store = store;
+ }
+
+ @Override
+ public void clear() {
+ store.clear();
+ }
+
+ @Override
+ public boolean containsKey(final KeyType key) {
+ if (store.containsKey(key)) return true;
+
+ return delegate.containsKey(key);
+ }
+
+ @Override
+ public IMap<KeyType, ValueType> extend() {
+ return new ExtendedMap<>(this, new FunctionalMap<>());
+ }
+
+ @Override
+ public void forEach(final BiConsumer<KeyType, ValueType> action) {
+ store.forEach(action);
+
+ delegate.forEach(action);
+ }
+
+ @Override
+ public void forEachKey(final Consumer<KeyType> action) {
+ store.forEachKey(action);
+
+ delegate.forEachKey(action);
+ }
+
+ @Override
+ public void forEachValue(final Consumer<ValueType> action) {
+ store.forEachValue(action);
+
+ delegate.forEachValue(action);
+ }
+
+ @Override
+ public ValueType get(final KeyType key) {
+ if (store.containsKey(key)) return store.get(key);
+
+ return delegate.get(key);
+ }
+
+ @Override
+ public int size() {
+ return store.size() + delegate.size();
+ }
+
+ @Override
+ public IList<KeyType> keyList() {
+ return ListUtils.mergeLists(store.keyList(), delegate.keyList());
+ }
+
+ @Override
+ public <MappedValue> IMap<KeyType, MappedValue> transform(final Function<ValueType, MappedValue> transformer) {
+ return new TransformedValueMap<>(this, transformer);
+ }
+
+ @Override
+ public ValueType put(final KeyType key, final ValueType val) {
+ return store.put(key, val);
+ }
+
+ @Override
+ public ValueType remove(final KeyType key) {
+ if (!store.containsKey(key)) return delegate.remove(key);
+
+ return store.remove(key);
+ }
+
+ @Override
+ public IList<ValueType> valueList() {
+ return ListUtils.mergeLists(store.valueList(), delegate.valueList());
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (delegate == null ? 0 : delegate.hashCode());
+ result = prime * result + (store == null ? 0 : store.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof ExtendedMap)) return false;
+
+ final ExtendedMap<?, ?> other = (ExtendedMap<?, ?>) obj;
+
+ if (delegate == null) {
+ if (other.delegate != null) return false;
+ } else if (!delegate.equals(other.delegate)) return false;
+ if (store == null) {
+ if (other.store != null) return false;
+ } else if (!store.equals(other.store)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ExtendedMap [delegate=%s, store=%s]", delegate, store);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalList.java b/base/src/main/java/bjc/utils/funcdata/FunctionalList.java
new file mode 100644
index 0000000..55ea7ff
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/FunctionalList.java
@@ -0,0 +1,423 @@
+package bjc.utils.funcdata;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.Pair;
+
+/**
+ * A wrapper over another list that provides eager functional operations over
+ * it.
+ *
+ * Differs from a stream in every way except for the fact that they both provide
+ * functional operations.
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type in this list
+ */
+public class FunctionalList<E> implements Cloneable, IList<E> {
+ /*
+ * The list used as a backing store
+ */
+ private final List<E> wrapped;
+
+ /**
+ * Create a new empty functional list.
+ */
+ public FunctionalList() {
+ wrapped = new ArrayList<>();
+ }
+
+ /**
+ * Create a new functional list containing the specified items.
+ *
+ * Takes O(n) time, where n is the number of items specified
+ *
+ * @param items
+ * The items to put into this functional list.
+ */
+ @SafeVarargs
+ public FunctionalList(final E... items) {
+ wrapped = new ArrayList<>(items.length);
+
+ for (final E item : items) {
+ wrapped.add(item);
+ }
+ }
+
+ /**
+ * Create a new functional list with the specified size.
+ *
+ * @param size
+ * The size of the backing list .
+ */
+ private FunctionalList(final int size) {
+ wrapped = new ArrayList<>(size);
+ }
+
+ /**
+ * Create a new functional list as a wrapper of a existing list.
+ *
+ * Takes O(1) time, since it doesn't copy the list.
+ *
+ * @param backing
+ * The list to use as a backing list.
+ */
+ public FunctionalList(final List<E> backing) {
+ if (backing == null) throw new NullPointerException("Backing list must be non-null");
+
+ wrapped = backing;
+ }
+
+ @Override
+ public boolean add(final E item) {
+ return wrapped.add(item);
+ }
+
+ @Override
+ public boolean allMatch(final Predicate<E> predicate) {
+ if (predicate == null) throw new NullPointerException("Predicate must be non-null");
+
+ for (final E item : wrapped) {
+ if (!predicate.test(item))
+ // We've found a non-matching item
+ return false;
+ }
+
+ // All of the items matched
+ return true;
+ }
+
+ @Override
+ public boolean anyMatch(final Predicate<E> predicate) {
+ if (predicate == null) throw new NullPointerException("Predicate must be not null");
+
+ for (final E item : wrapped) {
+ if (predicate.test(item))
+ // We've found a matching item
+ return true;
+ }
+
+ // We didn't find a matching item
+ return false;
+ }
+
+ /**
+ * Clone this list into a new one, and clone the backing list as well
+ *
+ * Takes O(n) time, where n is the number of elements in the list
+ *
+ * @return A list
+ */
+ @Override
+ public IList<E> clone() {
+ final IList<E> cloned = new FunctionalList<>();
+
+ for (final E element : wrapped) {
+ cloned.add(element);
+ }
+
+ return cloned;
+ }
+
+ @Override
+ public <T, F> IList<F> combineWith(final IList<T> rightList, final BiFunction<E, T, F> itemCombiner) {
+ if (rightList == null)
+ throw new NullPointerException("Target combine list must not be null");
+ else if (itemCombiner == null) throw new NullPointerException("Combiner must not be null");
+
+ final IList<F> returned = new FunctionalList<>();
+
+ // Get the iterator for the other list
+ final Iterator<T> rightIterator = rightList.toIterable().iterator();
+
+ for (final Iterator<E> leftIterator = wrapped.iterator(); leftIterator.hasNext()
+ && rightIterator.hasNext();) {
+ // Add the transformed items to the result list
+ final E leftVal = leftIterator.next();
+ final T rightVal = rightIterator.next();
+
+ returned.add(itemCombiner.apply(leftVal, rightVal));
+ }
+
+ return returned;
+ }
+
+ @Override
+ public boolean contains(final E item) {
+ // Check if any items in the list match the provided item
+ return this.anyMatch(item::equals);
+ }
+
+ @Override
+ public E first() {
+ if (wrapped.size() < 1)
+ throw new NoSuchElementException("Attempted to get first element of empty list");
+
+ return wrapped.get(0);
+ }
+
+ @Override
+ public <T> IList<T> flatMap(final Function<E, IList<T>> expander) {
+ if (expander == null) throw new NullPointerException("Expander must not be null");
+
+ final IList<T> returned = new FunctionalList<>(this.wrapped.size());
+
+ forEach(element -> {
+ final IList<T> expandedElement = expander.apply(element);
+
+ if (expandedElement == null) throw new NullPointerException("Expander returned null list");
+
+ // Add each element to the returned list
+ expandedElement.forEach(returned::add);
+ });
+
+ return returned;
+ }
+
+ @Override
+ public void forEach(final Consumer<? super E> action) {
+ if (action == null) throw new NullPointerException("Action is null");
+
+ wrapped.forEach(action);
+ }
+
+ @Override
+ public void forEachIndexed(final BiConsumer<Integer, E> indexedAction) {
+ if (indexedAction == null) throw new NullPointerException("Action must not be null");
+
+ // This is held b/c ref'd variables must be final/effectively
+ // final
+ final IHolder<Integer> currentIndex = new Identity<>(0);
+
+ wrapped.forEach((element) -> {
+ // Call the action with the index and the value
+ indexedAction.accept(currentIndex.unwrap(index -> index), element);
+
+ // Increment the value
+ currentIndex.transform((index) -> index + 1);
+ });
+ }
+
+ @Override
+ public E getByIndex(final int index) {
+ return wrapped.get(index);
+ }
+
+ /**
+ * Get the internal backing list.
+ *
+ * @return The backing list this list is based off of.
+ */
+ public List<E> getInternal() {
+ return wrapped;
+ }
+
+ @Override
+ public IList<E> getMatching(final Predicate<E> predicate) {
+ if (predicate == null) throw new NullPointerException("Predicate must not be null");
+
+ final IList<E> returned = new FunctionalList<>();
+
+ wrapped.forEach((element) -> {
+ if (predicate.test(element)) {
+ // The item matches, so add it to the returned
+ // list
+ returned.add(element);
+ }
+ });
+
+ return returned;
+ }
+
+ @Override
+ public int getSize() {
+ return wrapped.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return wrapped.isEmpty();
+ }
+
+ /*
+ * Check if a partition has room for another item
+ */
+ private Boolean isPartitionFull(final int numberPerPartition, final IHolder<IList<E>> currentPartition) {
+ return currentPartition.unwrap((partition) -> partition.getSize() >= numberPerPartition);
+ }
+
+ @Override
+ public <T> IList<T> map(final Function<E, T> elementTransformer) {
+ if (elementTransformer == null) throw new NullPointerException("Transformer must be not null");
+
+ final IList<T> returned = new FunctionalList<>(this.wrapped.size());
+
+ forEach(element -> {
+ // Add the transformed item to the result
+ returned.add(elementTransformer.apply(element));
+ });
+
+ return returned;
+ }
+
+ @Override
+ public <T> IList<IPair<E, T>> pairWith(final IList<T> rightList) {
+ return combineWith(rightList, Pair<E, T>::new);
+ }
+
+ @Override
+ public IList<IList<E>> partition(final int numberPerPartition) {
+ if (numberPerPartition < 1 || numberPerPartition > wrapped.size()) {
+ final String fmt = "%s is an invalid partition size. Must be between 1 and %d";
+ final String msg = String.format(fmt, numberPerPartition, wrapped.size());
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final IList<IList<E>> returned = new FunctionalList<>();
+
+ // The current partition being filled
+ final IHolder<IList<E>> currentPartition = new Identity<>(new FunctionalList<>());
+
+ this.forEach(element -> {
+ if (isPartitionFull(numberPerPartition, currentPartition)) {
+ // Add the partition to the list
+ returned.add(currentPartition.unwrap(partition -> partition));
+
+ // Start a new partition
+ currentPartition.transform(partition -> new FunctionalList<>());
+ } else {
+ // Add the element to the current partition
+ currentPartition.unwrap(partition -> partition.add(element));
+ }
+ });
+
+ return returned;
+ }
+
+ @Override
+ public void prepend(final E item) {
+ wrapped.add(0, item);
+ }
+
+ @Override
+ public E randItem(final Function<Integer, Integer> rnd) {
+ if (rnd == null) throw new NullPointerException("Random source must not be null");
+
+ final int randomIndex = rnd.apply(wrapped.size());
+
+ return wrapped.get(randomIndex);
+ }
+
+ @Override
+ public <T, F> F reduceAux(final T initialValue, final BiFunction<E, T, T> stateAccumulator,
+ final Function<T, F> resultTransformer) {
+ if (stateAccumulator == null)
+ throw new NullPointerException("Accumulator must not be null");
+ else if (resultTransformer == null) throw new NullPointerException("Transformer must not be null");
+
+ // The current collapsed list
+ final IHolder<T> currentState = new Identity<>(initialValue);
+
+ wrapped.forEach(element -> {
+ // Accumulate a new value into the state
+ currentState.transform(state -> stateAccumulator.apply(element, state));
+ });
+
+ // Convert the state to its final value
+ return currentState.unwrap(resultTransformer);
+ }
+
+ @Override
+ public boolean removeIf(final Predicate<E> removePredicate) {
+ if (removePredicate == null) throw new NullPointerException("Predicate must be non-null");
+
+ return wrapped.removeIf(removePredicate);
+ }
+
+ @Override
+ public void removeMatching(final E desiredElement) {
+ removeIf(element -> element.equals(desiredElement));
+ }
+
+ @Override
+ public void reverse() {
+ Collections.reverse(wrapped);
+ }
+
+ @Override
+ public E search(final E searchKey, final Comparator<E> comparator) {
+ // Search our internal list
+ final int foundIndex = Collections.binarySearch(wrapped, searchKey, comparator);
+
+ if (foundIndex >= 0) // We found a matching element
+ return wrapped.get(foundIndex);
+
+ // We didn't find an element
+ return null;
+ }
+
+ @Override
+ public void sort(final Comparator<E> comparator) {
+ // sb.deleteCharAt(sb.length() - 2);
+ Collections.sort(wrapped, comparator);
+ }
+
+ @Override
+ public IList<E> tail() {
+ return new FunctionalList<>(wrapped.subList(1, getSize()));
+ }
+
+ @Override
+ public E[] toArray(final E[] arrType) {
+ return wrapped.toArray(arrType);
+ }
+
+ @Override
+ public Iterable<E> toIterable() {
+ return wrapped;
+ }
+
+ @Override
+ public String toString() {
+ final int lSize = getSize();
+
+ if (lSize == 0) return "()";
+
+ final StringBuilder sb = new StringBuilder("(");
+ final Iterator<E> itr = toIterable().iterator();
+ final E itm = itr.next();
+ int i = 0;
+
+ if (lSize == 1) return "(" + itm + ")";
+
+ for (final E item : toIterable()) {
+ sb.append(item.toString());
+
+ if (i < lSize - 1) {
+ sb.append(", ");
+ }
+
+ i += 1;
+ }
+
+ sb.append(")");
+
+ return sb.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java b/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java
new file mode 100644
index 0000000..c4f0ff1
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/FunctionalMap.java
@@ -0,0 +1,175 @@
+package bjc.utils.funcdata;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import bjc.utils.data.IPair;
+
+/**
+ * Basic implementation of {@link IMap}
+ *
+ * @author ben
+ *
+ * @param <KeyType>
+ * The type of the map's keys
+ * @param <ValueType>
+ * The type of the map's values
+ */
+public class FunctionalMap<KeyType, ValueType> implements IMap<KeyType, ValueType> {
+ private Map<KeyType, ValueType> wrappedMap;
+
+ /**
+ * Create a new blank functional map
+ */
+ public FunctionalMap() {
+ wrappedMap = new HashMap<>();
+ }
+
+ /**
+ * Create a new functional map with the specified entries
+ *
+ * @param entries
+ * The entries to put into the map
+ */
+ @SafeVarargs
+ public FunctionalMap(final IPair<KeyType, ValueType>... entries) {
+ this();
+
+ for (final IPair<KeyType, ValueType> entry : entries) {
+ entry.doWith((key, val) -> {
+ wrappedMap.put(key, val);
+ });
+ }
+ }
+
+ /**
+ * Create a new functional map wrapping the specified map
+ *
+ * @param wrap
+ * The map to wrap
+ */
+ public FunctionalMap(final Map<KeyType, ValueType> wrap) {
+ if (wrap == null) throw new NullPointerException("Map to wrap must not be null");
+
+ wrappedMap = wrap;
+ }
+
+ @Override
+ public void clear() {
+ wrappedMap.clear();
+ }
+
+ @Override
+ public boolean containsKey(final KeyType key) {
+ return wrappedMap.containsKey(key);
+ }
+
+ @Override
+ public IMap<KeyType, ValueType> extend() {
+ return new ExtendedMap<>(this, new FunctionalMap<>());
+ }
+
+ @Override
+ public void forEach(final BiConsumer<KeyType, ValueType> action) {
+ wrappedMap.forEach(action);
+ }
+
+ @Override
+ public void forEachKey(final Consumer<KeyType> action) {
+ wrappedMap.keySet().forEach(action);
+ }
+
+ @Override
+ public void forEachValue(final Consumer<ValueType> action) {
+ wrappedMap.values().forEach(action);
+ }
+
+ @Override
+ public ValueType get(final KeyType key) {
+ if (key == null) throw new NullPointerException("Key must not be null");
+
+ if (!wrappedMap.containsKey(key)) {
+ final String msg = String.format("Key %s is not present in the map", key);
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ return wrappedMap.get(key);
+ }
+
+ @Override
+ public int size() {
+ return wrappedMap.size();
+ }
+
+ @Override
+ public IList<KeyType> keyList() {
+ final FunctionalList<KeyType> keys = new FunctionalList<>();
+
+ wrappedMap.keySet().forEach(key -> {
+ keys.add(key);
+ });
+
+ return keys;
+ }
+
+ @Override
+ public <MappedValue> IMap<KeyType, MappedValue> transform(final Function<ValueType, MappedValue> transformer) {
+ if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ return new TransformedValueMap<>(this, transformer);
+ }
+
+ @Override
+ public ValueType put(final KeyType key, final ValueType val) {
+ if (key == null) throw new NullPointerException("Key must not be null");
+
+ return wrappedMap.put(key, val);
+ }
+
+ @Override
+ public ValueType remove(final KeyType key) {
+ return wrappedMap.remove(key);
+ }
+
+ @Override
+ public String toString() {
+ return wrappedMap.toString();
+ }
+
+ @Override
+ public IList<ValueType> valueList() {
+ final FunctionalList<ValueType> values = new FunctionalList<>();
+
+ wrappedMap.values().forEach(value -> {
+ values.add(value);
+ });
+
+ return values;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (wrappedMap == null ? 0 : wrappedMap.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof FunctionalMap)) return false;
+
+ final FunctionalMap<?, ?> other = (FunctionalMap<?, ?>) obj;
+
+ if (wrappedMap == null) {
+ if (other.wrappedMap != null) return false;
+ } else if (!wrappedMap.equals(other.wrappedMap)) return false;
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java b/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java
new file mode 100644
index 0000000..e068b46
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/FunctionalStringTokenizer.java
@@ -0,0 +1,159 @@
+package bjc.utils.funcdata;
+
+import java.util.StringTokenizer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * A string tokenizer that exposes a functional interface
+ *
+ * @author ben
+ *
+ */
+public class FunctionalStringTokenizer {
+ /**
+ * Create a new tokenizer from the specified string.
+ *
+ * @param strang
+ * The string to create a tokenizer from.
+ * @return A new tokenizer that splits the provided string on spaces.
+ */
+ public static FunctionalStringTokenizer fromString(final String strang) {
+ if (strang == null) throw new NullPointerException("String to tokenize must be non-null");
+
+ return new FunctionalStringTokenizer(new StringTokenizer(strang, " "));
+ }
+
+ /*
+ * The string tokenizer being driven
+ */
+ private final StringTokenizer input;
+
+ /**
+ * Create a functional string tokenizer from a given string
+ *
+ * @param inp
+ * The string to tokenize
+ */
+ public FunctionalStringTokenizer(final String inp) {
+ if (inp == null) throw new NullPointerException("String to tokenize must be non-null");
+
+ this.input = new StringTokenizer(inp);
+ }
+
+ /**
+ * Create a functional string tokenizer from a given string and set of
+ * separators
+ *
+ * @param input
+ * The string to tokenize
+ * @param seperators
+ * The set of separating tokens to use for splitting
+ */
+ public FunctionalStringTokenizer(final String input, final String seperators) {
+ if (input == null)
+ throw new NullPointerException("String to tokenize must not be null");
+ else if (seperators == null) throw new NullPointerException("Tokens to split on must not be null");
+
+ this.input = new StringTokenizer(input, seperators);
+ }
+
+ /**
+ * Create a functional string tokenizer from a non-functional one
+ *
+ * @param toWrap
+ * The non-functional string tokenizer to wrap
+ */
+ public FunctionalStringTokenizer(final StringTokenizer toWrap) {
+ if (toWrap == null) throw new NullPointerException("Wrapped tokenizer must not be null");
+
+ this.input = toWrap;
+ }
+
+ /**
+ * Execute a provided action for each of the remaining tokens
+ *
+ * @param action
+ * The action to execute for each token
+ */
+ public void forEachToken(final Consumer<String> action) {
+ if (action == null) throw new NullPointerException("Action must not be null");
+
+ while (input.hasMoreTokens()) {
+ action.accept(input.nextToken());
+ }
+ }
+
+ /**
+ * Get the string tokenizer encapsulated by this tokenizer
+ *
+ * @return The encapsulated tokenizer
+ */
+ public StringTokenizer getInternal() {
+ return input;
+ }
+
+ /**
+ * Check if this tokenizer has more tokens
+ *
+ * @return Whether or not this tokenizer has more tokens
+ */
+ public boolean hasMoreTokens() {
+ return input.hasMoreTokens();
+ }
+
+ /**
+ * Return the next token from the tokenizer.
+ *
+ * Returns null if no more tokens are available
+ *
+ * @return The next token from the tokenizer
+ */
+ public String nextToken() {
+ if (input.hasMoreTokens()) // Return the next available token
+ return input.nextToken();
+
+ // Return no token
+ return null;
+ }
+
+ /**
+ * Convert this tokenizer into a list of strings
+ *
+ * @return This tokenizer, converted into a list of strings
+ */
+ public IList<String> toList() {
+ return toList((final String element) -> element);
+ }
+
+ /**
+ * Convert the contents of this tokenizer into a list. Consumes all of
+ * the input from this tokenizer.
+ *
+ * @param <E>
+ * The type of the converted tokens
+ *
+ * @param transformer
+ * The function to use to convert tokens.
+ * @return A list containing all of the converted tokens.
+ */
+ public <E> IList<E> toList(final Function<String, E> transformer) {
+ if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ final IList<E> returned = new FunctionalList<>();
+
+ // Add each token to the list after transforming it
+ forEachToken(token -> {
+ final E transformedToken = transformer.apply(token);
+
+ returned.add(transformedToken);
+ });
+
+ return returned;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FunctionalStringTokenizer [input=%s]", input);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/IList.java b/base/src/main/java/bjc/utils/funcdata/IList.java
new file mode 100644
index 0000000..28c09d0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/IList.java
@@ -0,0 +1,416 @@
+package bjc.utils.funcdata;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+
+import bjc.utils.data.IPair;
+import bjc.utils.functypes.ID;
+
+/**
+ * A wrapper over another list that provides functional operations over it.
+ *
+ * @author ben
+ *
+ * @param <ContainedType>
+ * The type in this list
+ */
+public interface IList<ContainedType> extends Iterable<ContainedType> {
+ /**
+ * Add an item to this list
+ *
+ * @param item
+ * The item to add to this list.
+ * @return Whether the item was added to the list successfully.
+ */
+ boolean add(ContainedType item);
+
+ /**
+ * Add all of the elements in the provided list to this list
+ *
+ * @param items
+ * The list of items to add
+ * @return True if every item was successfully added to the list, false
+ * otherwise
+ */
+ default boolean addAll(final IList<ContainedType> items) {
+ return items.map(this::add).anyMatch(bl -> bl == false);
+ }
+
+ /**
+ * Add all of the elements in the provided array to this list.
+ *
+ * @param items
+ * The array of items to add.
+ *
+ * @return True if every item was successfully added to the list, false
+ * otherwise.
+ */
+ @SuppressWarnings("unchecked")
+ default boolean addAll(final ContainedType... items) {
+ boolean succ = true;
+
+ for (final ContainedType item : items) {
+ final boolean addSucc = add(item);
+
+ succ = succ ? addSucc : false;
+ }
+
+ return succ;
+ }
+
+ /**
+ * Check if all of the elements of this list match the specified
+ * predicate.
+ *
+ * @param matcher
+ * The predicate to use for checking.
+ * @return Whether all of the elements of the list match the specified
+ * predicate.
+ */
+ boolean allMatch(Predicate<ContainedType> matcher);
+
+ /**
+ * Check if any of the elements in this list match the specified list.
+ *
+ * @param matcher
+ * The predicate to use for checking.
+ * @return Whether any element in the list matches the provided
+ * predicate.
+ */
+ boolean anyMatch(Predicate<ContainedType> matcher);
+
+ /**
+ * Reduce the contents of this list using a collector
+ *
+ * @param <StateType>
+ * The intermediate accumulation type
+ * @param <ReducedType>
+ * The final, reduced type
+ * @param collector
+ * The collector to use for reduction
+ * @return The reduced list
+ */
+ default <StateType, ReducedType> ReducedType collect(
+ final Collector<ContainedType, StateType, ReducedType> collector) {
+ final BiConsumer<StateType, ContainedType> accumulator = collector.accumulator();
+
+ final StateType initial = collector.supplier().get();
+ return reduceAux(initial, (value, state) -> {
+ accumulator.accept(state, value);
+
+ return state;
+ }, collector.finisher());
+ }
+
+ /**
+ * Combine this list with another one into a new list and merge the
+ * results.
+ *
+ * Works sort of like a combined zip/map over resulting pairs. Does not
+ * change the underlying list.
+ *
+ * NOTE: The returned list will have the length of the shorter of this
+ * list and the combined one.
+ *
+ * @param <OtherType>
+ * The type of the second list
+ * @param <CombinedType>
+ * The type of the combined list
+ *
+ * @param list
+ * The list to combine with
+ * @param combiner
+ * The function to use for combining element pairs.
+ * @return A new list containing the merged pairs of lists.
+ */
+ <OtherType, CombinedType> IList<CombinedType> combineWith(IList<OtherType> list,
+ BiFunction<ContainedType, OtherType, CombinedType> combiner);
+
+ /**
+ * Check if the list contains the specified item
+ *
+ * @param item
+ * The item to see if it is contained
+ * @return Whether or not the specified item is in the list
+ */
+ boolean contains(ContainedType item);
+
+ /**
+ * Get the first element in the list
+ *
+ * @return The first element in this list.
+ */
+ ContainedType first();
+
+ /**
+ * Apply a function to each member of the list, then flatten the
+ * results.
+ *
+ * Does not change the underlying list.
+ *
+ * @param <MappedType>
+ * The type of the flattened list
+ *
+ * @param expander
+ * The function to apply to each member of the list.
+ * @return A new list containing the flattened results of applying the
+ * provided function.
+ */
+ <MappedType> IList<MappedType> flatMap(Function<ContainedType, IList<MappedType>> expander);
+
+ /**
+ * Apply a given action for each member of the list
+ *
+ * @param action
+ * The action to apply to each member of the list.
+ */
+ @Override
+ void forEach(Consumer<? super ContainedType> action);
+
+ /**
+ * Apply a given function to each element in the list and its index.
+ *
+ * @param action
+ * The function to apply to each element in the list and
+ * its index.
+ */
+ void forEachIndexed(BiConsumer<Integer, ContainedType> action);
+
+ /**
+ * Retrieve a value in the list by its index.
+ *
+ * @param index
+ * The index to retrieve a value from.
+ * @return The value at the specified index in the list.
+ */
+ ContainedType getByIndex(int index);
+
+ /**
+ * Retrieve a list containing all elements matching a predicate
+ *
+ * @param predicate
+ * The predicate to match by
+ * @return A list containing all elements that match the predicate
+ */
+ IList<ContainedType> getMatching(Predicate<ContainedType> predicate);
+
+ /**
+ * Retrieve the size of the wrapped list
+ *
+ * @return The size of the wrapped list
+ */
+ int getSize();
+
+ /**
+ * Check if this list is empty.
+ *
+ * @return Whether or not this list is empty.
+ */
+ boolean isEmpty();
+
+ /**
+ * Create a new list by applying the given function to each element in
+ * the list.
+ *
+ * Does not change the underlying list.
+ *
+ * @param <MappedType>
+ * The type of the transformed list
+ *
+ * @param transformer
+ * The function to apply to each element in the list
+ * @return A new list containing the mapped elements of this list.
+ */
+ <MappedType> IList<MappedType> map(Function<ContainedType, MappedType> transformer);
+
+ /**
+ * Zip two lists into a list of pairs
+ *
+ * @param <OtherType>
+ * The type of the second list
+ *
+ * @param list
+ * The list to use as the left side of the pair
+ * @return A list containing pairs of this element and the specified
+ * list
+ */
+ <OtherType> IList<IPair<ContainedType, OtherType>> pairWith(IList<OtherType> list);
+
+ /**
+ * Partition this list into a list of sublists
+ *
+ * @param partitionSize
+ * The size of elements to put into each one of the
+ * sublists
+ * @return A list partitioned into partitions of size nPerPart
+ */
+ IList<IList<ContainedType>> partition(int partitionSize);
+
+ /**
+ * Prepend an item to the list
+ *
+ * @param item
+ * The item to prepend to the list
+ */
+ void prepend(ContainedType item);
+
+ /**
+ * Prepend an array of items to the list.
+ *
+ * @param items
+ * The items to prepend to the list.
+ */
+ @SuppressWarnings("unchecked")
+ default void prependAll(final ContainedType... items) {
+ for (final ContainedType item : items) {
+ prepend(item);
+ }
+ }
+
+ /**
+ * Select a random item from the list, using a default random number
+ * generator
+ *
+ * @return A random item from the list
+ */
+ default ContainedType randItem() {
+ return randItem(num -> (int) (Math.random() * num));
+ }
+
+ /**
+ * Select a random item from this list, using the provided random number
+ * generator.
+ *
+ * @param rnd
+ * The random number generator to use.
+ * @return A random element from this list.
+ */
+ ContainedType randItem(Function<Integer, Integer> rnd);
+
+ /**
+ * Reduce this list to a single value, using a accumulative approach.
+ *
+ * @param <StateType>
+ * The in-between type of the values
+ * @param <ReducedType>
+ * The final value type
+ *
+ * @param initial
+ * The initial value of the accumulative state.
+ * @param accumulator
+ * The function to use to combine a list element with the
+ * accumulative state.
+ * @param transformer
+ * The function to use to convert the accumulative state
+ * into a final result.
+ * @return A single value condensed from this list and transformed into
+ * its final state.
+ */
+ <StateType, ReducedType> ReducedType reduceAux(StateType initial,
+ BiFunction<ContainedType, StateType, StateType> accumulator,
+ Function<StateType, ReducedType> transformer);
+
+ /**
+ * Reduce this list to a single value, using a accumulative approach.
+ *
+ * @param <StateType>
+ * The in-between type of the values.
+ *
+ * @param initial
+ * The initial value of the accumulative state.
+ *
+ * @param accumulator
+ * The function to use to combine a list element with the
+ * accumulative state.
+ *
+ * @return A single value condensed from this list.
+ */
+ default <StateType> StateType reduceAux(StateType initial,
+ BiFunction<ContainedType, StateType, StateType> accumulator) {
+ return reduceAux(initial, accumulator, ID.id());
+ }
+
+ /**
+ * Remove all elements that match a given predicate
+ *
+ * @param predicate
+ * The predicate to use to determine elements to delete
+ * @return Whether there was anything that satisfied the predicate
+ */
+ boolean removeIf(Predicate<ContainedType> predicate);
+
+ /**
+ * Remove all parameters that match a given parameter
+ *
+ * @param element
+ * The object to remove all matching copies of
+ */
+ void removeMatching(ContainedType element);
+
+ /**
+ * Reverse the contents of this list in place
+ */
+ void reverse();
+
+ /**
+ * Perform a binary search for the specified key using the provided
+ * means of comparing elements.
+ *
+ * Since this IS a binary search, the list must have been sorted before
+ * hand.
+ *
+ * @param key
+ * The key to search for.
+ * @param comparator
+ * The way to compare elements for searching. Pass null
+ * to use the natural ordering for E
+ * @return The element if it is in this list, or null if it is not.
+ */
+ ContainedType search(ContainedType key, Comparator<ContainedType> comparator);
+
+ /**
+ * Sort the elements of this list using the provided way of comparing
+ * elements.
+ *
+ * Does change the underlying list.
+ *
+ * @param comparator
+ * The way to compare elements for sorting. Pass null to
+ * use E's natural ordering
+ */
+ void sort(Comparator<ContainedType> comparator);
+
+ /**
+ * Get the tail of this list (the list without the first element
+ *
+ * @return The list without the first element
+ */
+ IList<ContainedType> tail();
+
+ /**
+ * Convert this list into an array
+ *
+ * @param type
+ * The type of array to return
+ * @return The list, as an array
+ */
+ ContainedType[] toArray(ContainedType[] type);
+
+ /**
+ * Convert the list into a Iterable
+ *
+ * @return An iterable view onto the list
+ */
+ Iterable<ContainedType> toIterable();
+
+ @Override
+ default Iterator<ContainedType> iterator() {
+ return toIterable().iterator();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/IMap.java b/base/src/main/java/bjc/utils/funcdata/IMap.java
new file mode 100644
index 0000000..0ee7375
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/IMap.java
@@ -0,0 +1,188 @@
+package bjc.utils.funcdata;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Functional wrapper over map providing some useful things.
+ *
+ * @author ben
+ *
+ * @param <KeyType>
+ * The type of this map's keys.
+ *
+ * @param <ValueType>
+ * The type of this map's values.
+ */
+public interface IMap<KeyType, ValueType> {
+ /**
+ * Execute an action for each entry in the map.
+ *
+ * @param action
+ * the action to execute for each entry in the map.
+ */
+ void forEach(BiConsumer<KeyType, ValueType> action);
+
+ /**
+ * Perform an action for each key in the map.
+ *
+ * @param action
+ * The action to perform on each key in the map.
+ */
+ default void forEachKey(final Consumer<KeyType> action) {
+ forEach((key, val) -> action.accept(key));
+ }
+
+ /**
+ * Perform an action for each value in the map.
+ *
+ * @param action
+ * The action to perform on each value in the map.
+ */
+ default void forEachValue(final Consumer<ValueType> action) {
+ forEach((key, val) -> action.accept(val));
+ }
+
+ /**
+ * Check if this map contains the specified key.
+ *
+ * @param key
+ * The key to check.
+ *
+ * @return Whether or not the map contains the key.
+ */
+ boolean containsKey(KeyType key);
+
+ /**
+ * Get the value assigned to the given key.
+ *
+ * @param key
+ * The key to look for a value under.
+ *
+ * @return The value of the key.
+ */
+ ValueType get(KeyType key);
+
+ /**
+ * Get a value from the map, and return a default value if the key
+ * doesn't exist.
+ *
+ * @param key
+ * The key to attempt to retrieve.
+ *
+ * @param defaultValue
+ * The value to return if the key doesn't exist.
+ *
+ * @return The value associated with the key, or the default value if
+ * the key doesn't exist.
+ */
+ default ValueType getOrDefault(final KeyType key, final ValueType defaultValue) {
+ try {
+ return get(key);
+ } catch (final IllegalArgumentException iaex) {
+ /*
+ * We don't care about this, because it indicates a key
+ * is missing.
+ */
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Add an entry to the map.
+ *
+ * @param key
+ * The key to put the value under.
+ *
+ * @param val
+ * The value to add.
+ *
+ * @return The previous value of the key in the map, or null if the key
+ * wasn't in the map. However, note that it may also return null
+ * if the key was set to null.
+ *
+ * @throws UnsupportedOperationException
+ * if the map implementation doesn't support modifying
+ * the map
+ */
+ ValueType put(KeyType key, ValueType val);
+
+ /**
+ * Delete all the values in the map.
+ */
+ default void clear() {
+ keyList().forEach(key -> remove(key));
+ }
+
+ /**
+ * Get the number of entries in this map.
+ *
+ * @return The number of entries in this map.
+ */
+ default int size() {
+ return keyList().getSize();
+ }
+
+ /**
+ * Transform the values returned by this map.
+ *
+ * NOTE: This transform is applied once for each lookup of a value, so
+ * the transform passed should be a proper function, or things will
+ * likely not work as expected.
+ *
+ * @param <V2>
+ * The new type of returned values.
+ *
+ * @param transformer
+ * The function to use to transform values.
+ *
+ * @return The map where each value will be transformed after lookup.
+ */
+ default <V2> IMap<KeyType, V2> transform(final Function<ValueType, V2> transformer) {
+ return new TransformedValueMap<>(this, transformer);
+ }
+
+ /**
+ * Extends this map, creating a new map that will delegate queries to
+ * the map, but store any added values itself.
+ *
+ * @return An extended map.
+ */
+ IMap<KeyType, ValueType> extend();
+
+ /**
+ * Remove the value bound to the key.
+ *
+ * @param key
+ * The key to remove from the map.
+ *
+ * @return The previous value for the key in the map, or null if the key
+ * wasn't in the class. NOTE: Just because you received null,
+ * doesn't mean the map wasn't changed. It may mean that someone
+ * put a null value for that key into the map.
+ */
+ ValueType remove(KeyType key);
+
+ /**
+ * Get a list of all the keys in this map.
+ *
+ * @return A list of all the keys in this map.
+ */
+ IList<KeyType> keyList();
+
+ /**
+ * Get a list of the values in this map.
+ *
+ * @return A list of values in this map.
+ */
+ default IList<ValueType> valueList() {
+ final IList<ValueType> returns = new FunctionalList<>();
+
+ for (final KeyType key : keyList()) {
+ returns.add(get(key));
+ }
+
+ return returns;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/SentryList.java b/base/src/main/java/bjc/utils/funcdata/SentryList.java
new file mode 100644
index 0000000..c322743
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/SentryList.java
@@ -0,0 +1,41 @@
+package bjc.utils.funcdata;
+
+import java.util.List;
+
+/**
+ * A list that logs when items are inserted into it.
+ *
+ * @author bjculkin
+ *
+ * @param <T>
+ * The type of item in the list.
+ */
+public class SentryList<T> extends FunctionalList<T> {
+ /**
+ * Create a new sentry list.
+ */
+ public SentryList() {
+ super();
+ }
+
+ /**
+ * Create a new sentry list backed by an existing list.
+ *
+ * @param backing
+ * The backing list.
+ */
+ public SentryList(final List<T> backing) {
+ super(backing);
+ }
+
+ @Override
+ public boolean add(final T item) {
+ final boolean val = super.add(item);
+
+ if (val) {
+ System.out.println("Added item (" + item + ") to list");
+ }
+
+ return val;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java b/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java
new file mode 100644
index 0000000..0ca1fdc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/TransformedValueMap.java
@@ -0,0 +1,102 @@
+package bjc.utils.funcdata;
+
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * A map that transforms values from one type to another
+ *
+ * @author ben
+ *
+ * @param <OldKey>
+ * The type of the map's keys
+ * @param <OldValue>
+ * The type of the map's values
+ * @param <NewValue>
+ * The type of the transformed values
+ */
+final class TransformedValueMap<OldKey, OldValue, NewValue> implements IMap<OldKey, NewValue> {
+ private final IMap<OldKey, OldValue> backing;
+ private final Function<OldValue, NewValue> transformer;
+
+ public TransformedValueMap(final IMap<OldKey, OldValue> backingMap,
+ final Function<OldValue, NewValue> transform) {
+ backing = backingMap;
+ transformer = transform;
+ }
+
+ @Override
+ public void clear() {
+ backing.clear();
+ }
+
+ @Override
+ public boolean containsKey(final OldKey key) {
+ return backing.containsKey(key);
+ }
+
+ @Override
+ public IMap<OldKey, NewValue> extend() {
+ return new ExtendedMap<>(this, new FunctionalMap<>());
+ }
+
+ @Override
+ public void forEach(final BiConsumer<OldKey, NewValue> action) {
+ backing.forEach((key, value) -> {
+ action.accept(key, transformer.apply(value));
+ });
+ }
+
+ @Override
+ public void forEachKey(final Consumer<OldKey> action) {
+ backing.forEachKey(action);
+ }
+
+ @Override
+ public void forEachValue(final Consumer<NewValue> action) {
+ backing.forEachValue(value -> {
+ action.accept(transformer.apply(value));
+ });
+ }
+
+ @Override
+ public NewValue get(final OldKey key) {
+ return transformer.apply(backing.get(key));
+ }
+
+ @Override
+ public int size() {
+ return backing.size();
+ }
+
+ @Override
+ public IList<OldKey> keyList() {
+ return backing.keyList();
+ }
+
+ @Override
+ public <MappedValue> IMap<OldKey, MappedValue> transform(final Function<NewValue, MappedValue> transform) {
+ return new TransformedValueMap<>(this, transform);
+ }
+
+ @Override
+ public NewValue put(final OldKey key, final NewValue value) {
+ throw new UnsupportedOperationException("Can't add items to transformed map");
+ }
+
+ @Override
+ public NewValue remove(final OldKey key) {
+ return transformer.apply(backing.remove(key));
+ }
+
+ @Override
+ public String toString() {
+ return backing.toString();
+ }
+
+ @Override
+ public IList<NewValue> valueList() {
+ return backing.valueList().map(transformer);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java
new file mode 100644
index 0000000..8acd477
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTree.java
@@ -0,0 +1,221 @@
+package bjc.utils.funcdata.bst;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A binary search tree, with some mild support for functional traversal.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The data type stored in the node.
+ */
+public class BinarySearchTree<T> {
+ /*
+ * The comparator for use in ordering items
+ */
+ private final Comparator<T> comparator;
+
+ /*
+ * The current count of elements in the tree
+ */
+ private int elementCount;
+
+ /*
+ * The root element of the tree
+ */
+ private ITreePart<T> root;
+
+ /**
+ * Create a new tree using the specified way to compare elements.
+ *
+ * @param cmp
+ * The thing to use for comparing elements
+ */
+ public BinarySearchTree(final Comparator<T> cmp) {
+ if (cmp == null) throw new NullPointerException("Comparator must not be null");
+
+ elementCount = 0;
+ comparator = cmp;
+ }
+
+ /**
+ * Add a node to the binary search tree.
+ *
+ * @param element
+ * The data to add to the binary search tree.
+ */
+ public void addNode(final T element) {
+ elementCount++;
+
+ if (root == null) {
+ root = new BinarySearchTreeNode<>(element, null, null);
+ } else {
+ root.add(element, comparator);
+ }
+ }
+
+ /**
+ * Check if an adjusted pivot falls with the bounds of a list
+ *
+ * @param elements
+ * The list to get bounds from
+ * @param pivot
+ * The pivot
+ * @param pivotAdjustment
+ * The distance from the pivot
+ * @return Whether the adjusted pivot is with the list
+ */
+ private boolean adjustedPivotInBounds(final IList<T> elements, final int pivot, final int pivotAdjustment) {
+ return pivot - pivotAdjustment >= 0 && pivot + pivotAdjustment < elements.getSize();
+ }
+
+ /**
+ * Balance the tree, and remove soft-deleted nodes for free.
+ *
+ * Takes O(N) time, but also O(N) space.
+ */
+ public void balance() {
+ final IList<T> elements = new FunctionalList<>();
+
+ // Add each element to the list in sorted order
+ root.forEach(TreeLinearizationMethod.INORDER, element -> elements.add(element));
+
+ // Clear the tree
+ root = null;
+
+ // Set up the pivot and adjustment for readding elements
+ final int pivot = elements.getSize() / 2;
+ int pivotAdjustment = 0;
+
+ // Add elements until there aren't any left
+ while (adjustedPivotInBounds(elements, pivot, pivotAdjustment)) {
+ if (root == null) {
+ // Create a new root element
+ root = new BinarySearchTreeNode<>(elements.getByIndex(pivot), null, null);
+ } else {
+ // Add the left and right elements in a balanced
+ // manner
+ root.add(elements.getByIndex(pivot + pivotAdjustment), comparator);
+
+ root.add(elements.getByIndex(pivot - pivotAdjustment), comparator);
+ }
+
+ // Increase the distance from the pivot
+ pivotAdjustment++;
+ }
+
+ // Add any trailing unbalanced elements
+ if (pivot - pivotAdjustment >= 0) {
+ root.add(elements.getByIndex(pivot - pivotAdjustment), comparator);
+ } else if (pivot + pivotAdjustment < elements.getSize()) {
+ root.add(elements.getByIndex(pivot + pivotAdjustment), comparator);
+ }
+ }
+
+ /**
+ * Soft-delete a node from the tree.
+ *
+ * Soft-deleted nodes stay in the tree until trim()/balance() is
+ * invoked, and are not included in traversals/finds.
+ *
+ * @param element
+ * The node to delete
+ */
+ public void deleteNode(final T element) {
+ elementCount--;
+
+ root.delete(element, comparator);
+ }
+
+ /**
+ * Get the root of the tree.
+ *
+ * @return The root of the tree.
+ */
+ public ITreePart<T> getRoot() {
+ return root;
+ }
+
+ /**
+ * Check if a node is in the tree
+ *
+ * @param element
+ * The node to check the presence of for the tree.
+ * @return Whether or not the node is in the tree.
+ */
+ public boolean isInTree(final T element) {
+ return root.contains(element, comparator);
+ }
+
+ /**
+ * Traverse the tree in a specified way until the function fails
+ *
+ * @param linearizationMethod
+ * The way to linearize the tree for traversal
+ * @param traversalPredicate
+ * The function to use until it fails
+ */
+ public void traverse(final TreeLinearizationMethod linearizationMethod, final Predicate<T> traversalPredicate) {
+ if (linearizationMethod == null)
+ throw new NullPointerException("Linearization method must not be null");
+ else if (traversalPredicate == null) throw new NullPointerException("Predicate must not be nulls");
+
+ root.forEach(linearizationMethod, traversalPredicate);
+ }
+
+ /**
+ * Remove all soft-deleted nodes from the tree.
+ */
+ public void trim() {
+ final List<T> nodes = new ArrayList<>(elementCount);
+
+ // Add all non-soft deleted nodes to the tree in insertion order
+ traverse(TreeLinearizationMethod.PREORDER, node -> {
+ nodes.add(node);
+ return true;
+ });
+
+ // Clear the tree
+ root = null;
+
+ // Add the nodes to the tree in the order they were inserted
+ nodes.forEach(node -> addNode(node));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("BinarySearchTree [elementCount=%s, root='%s']", elementCount, root);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + elementCount;
+ result = prime * result + (root == null ? 0 : root.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof BinarySearchTree<?>)) return false;
+
+ final BinarySearchTree<?> other = (BinarySearchTree<?>) obj;
+
+ if (elementCount != other.elementCount) return false;
+ if (root == null) {
+ if (other.root != null) return false;
+ } else if (!root.equals(other.root)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java
new file mode 100644
index 0000000..8c4f3f0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeLeaf.java
@@ -0,0 +1,119 @@
+package bjc.utils.funcdata.bst;
+
+import java.util.Comparator;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A leaf in a tree.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The data stored in the tree.
+ */
+public class BinarySearchTreeLeaf<T> implements ITreePart<T> {
+ /**
+ * The data held in this tree leaf
+ */
+ protected T data;
+
+ /**
+ * Whether this node is soft-deleted or not
+ */
+ protected boolean isDeleted;
+
+ /**
+ * Create a new leaf holding the specified data.
+ *
+ * @param element
+ * The data for the leaf to hold.
+ */
+ public BinarySearchTreeLeaf(final T element) {
+ data = element;
+ }
+
+ @Override
+ public void add(final T element, final Comparator<T> comparator) {
+ throw new IllegalArgumentException("Can't add to a leaf.");
+ }
+
+ @Override
+ public <E> E collapse(final Function<T, E> leafTransformer, final BiFunction<E, E, E> branchCollapser) {
+ if (leafTransformer == null) throw new NullPointerException("Transformer must not be null");
+
+ return leafTransformer.apply(data);
+ }
+
+ @Override
+ public boolean contains(final T element, final Comparator<T> comparator) {
+ return this.data.equals(element);
+ }
+
+ @Override
+ public T data() {
+ return data;
+ }
+
+ @Override
+ public void delete(final T element, final Comparator<T> comparator) {
+ if (data.equals(element)) {
+ isDeleted = true;
+ }
+ }
+
+ @Override
+ public boolean directedWalk(final DirectedWalkFunction<T> treeWalker) {
+ if (treeWalker == null) throw new NullPointerException("Tree walker must not be null");
+
+ switch (treeWalker.walk(data)) {
+ case SUCCESS:
+ return true;
+ // We don't have any children to care about
+ case FAILURE:
+ case LEFT:
+ case RIGHT:
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean forEach(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ if (traversalPredicate == null) throw new NullPointerException("Predicate must not be null");
+
+ return traversalPredicate.test(data);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("BinarySearchTreeLeaf [data='%s', isDeleted=%s]", data, isDeleted);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (data == null ? 0 : data.hashCode());
+ result = prime * result + (isDeleted ? 1231 : 1237);
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof BinarySearchTreeLeaf<?>)) return false;
+
+ final BinarySearchTreeLeaf<?> other = (BinarySearchTreeLeaf<?>) obj;
+
+ if (data == null) {
+ if (other.data != null) return false;
+ } else if (!data.equals(other.data)) return false;
+ if (isDeleted != other.isDeleted) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java
new file mode 100644
index 0000000..9f45c17
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/BinarySearchTreeNode.java
@@ -0,0 +1,287 @@
+package bjc.utils.funcdata.bst;
+
+import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.FAILURE;
+import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.LEFT;
+import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.RIGHT;
+import static bjc.utils.funcdata.bst.DirectedWalkFunction.DirectedWalkResult.SUCCESS;
+
+import java.util.Comparator;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A binary node in a tree.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The data type stored in the tree.
+ */
+public class BinarySearchTreeNode<T> extends BinarySearchTreeLeaf<T> {
+ /*
+ * The left child of this node
+ */
+ private ITreePart<T> left;
+
+ /*
+ * The right child of this node
+ */
+ private ITreePart<T> right;
+
+ /**
+ * Create a new node with the specified data and children.
+ *
+ * @param element
+ * The data to store in this node.
+ * @param lft
+ * The left child of this node.
+ * @param rght
+ * The right child of this node.
+ */
+ public BinarySearchTreeNode(final T element, final ITreePart<T> lft, final ITreePart<T> rght) {
+ super(element);
+ this.left = lft;
+ this.right = rght;
+ }
+
+ @Override
+ public void add(final T element, final Comparator<T> comparator) {
+ if (comparator == null) throw new NullPointerException("Comparator must not be null");
+
+ switch (comparator.compare(data, element)) {
+ case -1:
+ if (left == null) {
+ left = new BinarySearchTreeNode<>(element, null, null);
+ } else {
+ left.add(element, comparator);
+ }
+ break;
+ case 0:
+ if (isDeleted) {
+ isDeleted = false;
+ } else throw new IllegalArgumentException("Can't add duplicate values");
+ break;
+ case 1:
+ if (right == null) {
+ right = new BinarySearchTreeNode<>(element, null, null);
+ } else {
+ right.add(element, comparator);
+ }
+ break;
+ default:
+ throw new IllegalStateException("Error: Comparator yielded invalid value");
+ }
+ }
+
+ @Override
+ public <E> E collapse(final Function<T, E> nodeCollapser, final BiFunction<E, E, E> branchCollapser) {
+ if (nodeCollapser == null || branchCollapser == null)
+ throw new NullPointerException("Collapser must not be null");
+
+ final E collapsedNode = nodeCollapser.apply(data);
+
+ if (left != null) {
+ final E collapsedLeftBranch = left.collapse(nodeCollapser, branchCollapser);
+
+ if (right != null) {
+ final E collapsedRightBranch = right.collapse(nodeCollapser, branchCollapser);
+
+ final E collapsedBranches = branchCollapser.apply(collapsedLeftBranch,
+ collapsedRightBranch);
+
+ return branchCollapser.apply(collapsedNode, collapsedBranches);
+ }
+
+ return branchCollapser.apply(collapsedNode, collapsedLeftBranch);
+ }
+
+ if (right != null) {
+ final E collapsedRightBranch = right.collapse(nodeCollapser, branchCollapser);
+
+ return branchCollapser.apply(collapsedNode, collapsedRightBranch);
+ }
+
+ return collapsedNode;
+ }
+
+ @Override
+ public boolean contains(final T element, final Comparator<T> comparator) {
+ if (comparator == null) throw new NullPointerException("Comparator must not be null");
+
+ return directedWalk(currentElement -> {
+ switch (comparator.compare(element, currentElement)) {
+ case -1:
+ return LEFT;
+ case 0:
+ return isDeleted ? FAILURE : SUCCESS;
+ case 1:
+ return RIGHT;
+ default:
+ return FAILURE;
+ }
+ });
+ }
+
+ @Override
+ public void delete(final T element, final Comparator<T> comparator) {
+ if (comparator == null) throw new NullPointerException("Comparator must not be null");
+
+ directedWalk(currentElement -> {
+ switch (comparator.compare(data, element)) {
+ case -1:
+ return left == null ? FAILURE : LEFT;
+ case 0:
+ isDeleted = true;
+ return FAILURE;
+ case 1:
+ return right == null ? FAILURE : RIGHT;
+ default:
+ return FAILURE;
+ }
+ });
+ }
+
+ @Override
+ public boolean directedWalk(final DirectedWalkFunction<T> treeWalker) {
+ if (treeWalker == null) throw new NullPointerException("Walker must not be null");
+
+ switch (treeWalker.walk(data)) {
+ case SUCCESS:
+ return true;
+ case LEFT:
+ return left.directedWalk(treeWalker);
+ case RIGHT:
+ return right.directedWalk(treeWalker);
+ case FAILURE:
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean forEach(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ if (linearizationMethod == null)
+ throw new NullPointerException("Linearization method must not be null");
+ else if (traversalPredicate == null) throw new NullPointerException("Predicate must not be null");
+
+ switch (linearizationMethod) {
+ case PREORDER:
+ return preorderTraverse(linearizationMethod, traversalPredicate);
+ case INORDER:
+ return inorderTraverse(linearizationMethod, traversalPredicate);
+ case POSTORDER:
+ return postorderTraverse(linearizationMethod, traversalPredicate);
+ default:
+ throw new IllegalArgumentException(
+ "Passed an incorrect TreeLinearizationMethod " + linearizationMethod + ". WAT");
+ }
+ }
+
+ private boolean inorderTraverse(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false;
+
+ if (!traverseElement(traversalPredicate)) return false;
+
+ if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false;
+
+ return true;
+ }
+
+ private boolean postorderTraverse(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false;
+
+ if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false;
+
+ if (!traverseElement(traversalPredicate)) return false;
+
+ return true;
+
+ }
+
+ private boolean preorderTraverse(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ if (!traverseElement(traversalPredicate)) return false;
+
+ if (!traverseLeftBranch(linearizationMethod, traversalPredicate)) return false;
+
+ if (!traverseRightBranch(linearizationMethod, traversalPredicate)) return false;
+
+ return true;
+ }
+
+ private boolean traverseElement(final Predicate<T> traversalPredicate) {
+ boolean nodeSuccesfullyTraversed;
+
+ if (isDeleted) {
+ nodeSuccesfullyTraversed = true;
+ } else {
+ nodeSuccesfullyTraversed = traversalPredicate.test(data);
+ }
+
+ return nodeSuccesfullyTraversed;
+ }
+
+ private boolean traverseLeftBranch(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ boolean leftSuccesfullyTraversed;
+
+ if (left == null) {
+ leftSuccesfullyTraversed = true;
+ } else {
+ leftSuccesfullyTraversed = left.forEach(linearizationMethod, traversalPredicate);
+ }
+
+ return leftSuccesfullyTraversed;
+ }
+
+ private boolean traverseRightBranch(final TreeLinearizationMethod linearizationMethod,
+ final Predicate<T> traversalPredicate) {
+ boolean rightSuccesfullyTraversed;
+
+ if (right == null) {
+ rightSuccesfullyTraversed = true;
+ } else {
+ rightSuccesfullyTraversed = right.forEach(linearizationMethod, traversalPredicate);
+ }
+
+ return rightSuccesfullyTraversed;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("BinarySearchTreeNode [left='%s', right='%s']", left, right);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + (left == null ? 0 : left.hashCode());
+ result = prime * result + (right == null ? 0 : right.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (!super.equals(obj)) return false;
+ if (!(obj instanceof BinarySearchTreeNode<?>)) return false;
+
+ final BinarySearchTreeNode<?> other = (BinarySearchTreeNode<?>) obj;
+
+ if (left == null) {
+ if (other.left != null) return false;
+ } else if (!left.equals(other.left)) return false;
+
+ if (right == null) {
+ if (other.right != null) return false;
+ } else if (!right.equals(other.right)) return false;
+
+ return true;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java b/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java
new file mode 100644
index 0000000..e11524a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/DirectedWalkFunction.java
@@ -0,0 +1,49 @@
+package bjc.utils.funcdata.bst;
+
+/**
+ * Represents a function for doing a directed walk of a binary tree.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The type of element stored in the walked tree
+ */
+@FunctionalInterface
+public interface DirectedWalkFunction<T> {
+ /**
+ * Represents the results used to direct a walk in a binary tree.
+ *
+ * @author ben
+ *
+ */
+ public enum DirectedWalkResult {
+ /**
+ * Specifies that the function has failed.
+ */
+ FAILURE,
+ /**
+ * Specifies that the function wants to move left in the tree
+ * next.
+ */
+ LEFT,
+ /**
+ * Specifies that the function wants to move right in the tree
+ * next.
+ */
+ RIGHT,
+ /**
+ * Specifies that the function has succesfully completed
+ *
+ */
+ SUCCESS
+ }
+
+ /**
+ * Perform a directed walk on a node of a tree.
+ *
+ * @param element
+ * The data stored in the node currently being visited
+ * @return The way the function wants the walk to go next.
+ */
+ public DirectedWalkResult walk(T element);
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java b/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java
new file mode 100644
index 0000000..3aa8880
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/ITreePart.java
@@ -0,0 +1,96 @@
+package bjc.utils.funcdata.bst;
+
+import java.util.Comparator;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A interface for the fundamental things that want to be part of a tree.
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The data contained in this part of the tree.
+ */
+public interface ITreePart<T> {
+ /**
+ * Add a element below this tree part somewhere.
+ *
+ * @param element
+ * The element to add below this tree part
+ * @param comparator
+ * The thing to use for comparing values to find where to
+ * insert the tree part.
+ */
+ public void add(T element, Comparator<T> comparator);
+
+ /**
+ * Collapses this tree part into a single value. Does not change the
+ * underlying tree.
+ *
+ * @param <E>
+ * The type of the final collapsed value
+ *
+ * @param nodeCollapser
+ * The function to use to transform data into mapped
+ * form.
+ * @param branchCollapser
+ * The function to use to collapse data in mapped form
+ * into a single value.
+ * @return A single value from collapsing the tree.
+ */
+ public <E> E collapse(Function<T, E> nodeCollapser, BiFunction<E, E, E> branchCollapser);
+
+ /**
+ * Check if this tre part or below it contains the specified data item
+ *
+ * @param element
+ * The data item to look for.
+ * @param comparator
+ * The comparator to use to search for the data item
+ * @return Whether or not the given item is contained in this tree part
+ * or its children.
+ */
+ public boolean contains(T element, Comparator<T> comparator);
+
+ /**
+ * Get the data associated with this tree part.
+ *
+ * @return The data associated with this tree part.
+ */
+ public T data();
+
+ /**
+ * Remove the given node from this tree part and any of its children.
+ *
+ * @param element
+ * The data item to remove.
+ * @param comparator
+ * The comparator to use to search for the data item.
+ */
+ public void delete(T element, Comparator<T> comparator);
+
+ /**
+ * Execute a directed walk through the tree.
+ *
+ * @param walker
+ * The function to use to direct the walk through the
+ * tree.
+ * @return Whether the directed walk finished successfully.
+ */
+ public boolean directedWalk(DirectedWalkFunction<T> walker);
+
+ /**
+ * Execute a provided function for each element of tree it succesfully
+ * completes for
+ *
+ * @param linearizationMethod
+ * The way to linearize the tree for executing
+ * @param predicate
+ * The predicate to apply to each element, where it
+ * returning false terminates traversal early
+ * @return Whether the traversal finished succesfully
+ */
+ public boolean forEach(TreeLinearizationMethod linearizationMethod, Predicate<T> predicate);
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java b/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java
new file mode 100644
index 0000000..0c83867
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/bst/TreeLinearizationMethod.java
@@ -0,0 +1,25 @@
+package bjc.utils.funcdata.bst;
+
+/**
+ * Represents the ways to linearize a tree for traversal.
+ *
+ * @author ben
+ *
+ */
+public enum TreeLinearizationMethod {
+ /**
+ * Visit the left side of this tree part, the tree part itself, and then
+ * the right part.
+ */
+ INORDER,
+ /**
+ * Visit the left side of this tree part, the right side, and then the
+ * tree part itself.
+ */
+ POSTORDER,
+ /**
+ * Visit the tree part itself, then the left side of tthis tree part and
+ * then the right part.
+ */
+ PREORDER
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java b/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java
new file mode 100644
index 0000000..13c1709
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/theory/Bifunctor.java
@@ -0,0 +1,139 @@
+package bjc.utils.funcdata.theory;
+
+import java.util.function.Function;
+
+/**
+ * A functor over a pair of heterogeneous types
+ *
+ * @author ben
+ * @param <LeftType>
+ * The type stored on the 'left' of the pair
+ * @param <RightType>
+ * The type stored on the 'right' of the pair
+ *
+ */
+public interface Bifunctor<LeftType, RightType> {
+ /**
+ * Alias for functor mapping.
+ *
+ * @author EVE
+ *
+ * @param <OldLeft>
+ * @param <OldRight>
+ * @param <NewLeft>
+ * @param <NewRight>
+ */
+ public interface BifunctorMap<OldLeft, OldRight, NewLeft, NewRight>
+ extends Function<Bifunctor<OldLeft, OldRight>, Bifunctor<NewLeft, NewRight>> {
+
+ }
+
+ /**
+ * Alias for left functor mapping.
+ *
+ * @author EVE
+ *
+ * @param <OldLeft>
+ * @param <OldRight>
+ * @param <NewLeft>
+ */
+ public interface LeftBifunctorMap<OldLeft, OldRight, NewLeft>
+ extends BifunctorMap<OldLeft, OldRight, NewLeft, OldRight> {
+
+ }
+
+ /**
+ * Alias for right functor mapping.
+ *
+ * @author EVE
+ *
+ * @param <OldLeft>
+ * @param <OldRight>
+ * @param <NewRight>
+ */
+ public interface RightBifunctorMap<OldLeft, OldRight, NewRight>
+ extends BifunctorMap<OldLeft, OldRight, OldLeft, NewRight> {
+
+ }
+
+ /**
+ * Lift a pair of functions to a single function that maps over both
+ * parts of a pair
+ *
+ * @param <OldLeft>
+ * The old left type of the pair
+ * @param <OldRight>
+ * The old right type of the pair
+ * @param <NewLeft>
+ * The new left type of the pair
+ * @param <NewRight>
+ * The new right type of the pair
+ * @param leftFunc
+ * The function that maps over the left of the pair
+ * @param rightFunc
+ * The function that maps over the right of the pair
+ * @return A function that maps over both parts of the pair
+ */
+ public default <OldLeft, OldRight, NewLeft, NewRight> BifunctorMap<OldLeft, OldRight, NewLeft, NewRight> bimap(
+ final Function<OldLeft, NewLeft> leftFunc, final Function<OldRight, NewRight> rightFunc) {
+ final BifunctorMap<OldLeft, OldRight, NewLeft, NewRight> bimappedFunc = (argPair) -> {
+ final LeftBifunctorMap<OldLeft, OldRight, NewLeft> leftMapper = argPair.fmapLeft(leftFunc);
+
+ final Bifunctor<NewLeft, OldRight> leftMappedFunctor = leftMapper.apply(argPair);
+ final RightBifunctorMap<NewLeft, OldRight, NewRight> rightMapper = leftMappedFunctor
+ .fmapRight(rightFunc);
+
+ return rightMapper.apply(leftMappedFunctor);
+ };
+
+ return bimappedFunc;
+ }
+
+ /**
+ * Lift a function to operate over the left part of this pair
+ *
+ * @param <OldLeft>
+ * The old left type of the pair
+ * @param <OldRight>
+ * The old right type of the pair
+ * @param <NewLeft>
+ * The new left type of the pair
+ * @param func
+ * The function to lift to work over the left side of the
+ * pair
+ * @return The function lifted to work over the left side of bifunctors
+ */
+ public <OldLeft, OldRight, NewLeft> LeftBifunctorMap<OldLeft, OldRight, NewLeft> fmapLeft(
+ Function<OldLeft, NewLeft> func);
+
+ /**
+ * Lift a function to operate over the right part of this pair
+ *
+ * @param <OldLeft>
+ * The old left type of the pair
+ * @param <OldRight>
+ * The old right type of the pair
+ * @param <NewRight>
+ * The new right type of the pair
+ * @param func
+ * The function to lift to work over the right side of
+ * the pair
+ * @return The function lifted to work over the right side of bifunctors
+ */
+ public <OldLeft, OldRight, NewRight> RightBifunctorMap<OldLeft, OldRight, NewRight> fmapRight(
+ Function<OldRight, NewRight> func);
+
+ /**
+ * Get the value contained on the left of this bifunctor
+ *
+ * @return The value on the left side of this bifunctor
+ */
+ public LeftType getLeft();
+
+ /**
+ * Get the value contained on the right of this bifunctor
+ *
+ * @return The value on the right of this bifunctor
+ */
+ public RightType getRight();
+}
diff --git a/base/src/main/java/bjc/utils/funcdata/theory/Functor.java b/base/src/main/java/bjc/utils/funcdata/theory/Functor.java
new file mode 100644
index 0000000..1c53284
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcdata/theory/Functor.java
@@ -0,0 +1,39 @@
+package bjc.utils.funcdata.theory;
+
+import java.util.function.Function;
+
+/**
+ * Represents a container or context some sort usually, but the precise
+ * definition is that it represents exactly what it is defined as
+ *
+ * @author ben
+ * @param <ContainedType>
+ * The value inside the functor
+ */
+public interface Functor<ContainedType> {
+ /**
+ * Converts a normal function to operate over values in a functor.
+ *
+ * N.B: Even though the type signature implies that you can apply the
+ * resulting function to any type of functor, it is only safe to call it
+ * on instances of the type of functor you called fmap on.
+ *
+ * @param <ArgType>
+ * The argument of the function
+ * @param <ReturnType>
+ * The return type of the function
+ * @param func
+ * The function to convert
+ * @return The passed in function converted to work over a particular
+ * type of functors
+ */
+ public <ArgType, ReturnType> Function<Functor<ArgType>, Functor<ReturnType>> fmap(
+ Function<ArgType, ReturnType> func);
+
+ /**
+ * Retrieve the thing inside this functor
+ *
+ * @return The thing inside this functor
+ */
+ public ContainedType getValue();
+}
diff --git a/base/src/main/java/bjc/utils/functypes/ID.java b/base/src/main/java/bjc/utils/functypes/ID.java
new file mode 100644
index 0000000..d3197e2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/functypes/ID.java
@@ -0,0 +1,20 @@
+package bjc.utils.functypes;
+
+import java.util.function.UnaryOperator;
+
+/**
+ * Identity function.
+ *
+ * @author bjculkin
+ *
+ */
+public class ID {
+ /**
+ * Create an identity function.
+ *
+ * @return A identity function.
+ */
+ public static <A> UnaryOperator<A> id() {
+ return (x) -> x;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/functypes/ListFlattener.java b/base/src/main/java/bjc/utils/functypes/ListFlattener.java
new file mode 100644
index 0000000..cfa0c8b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/functypes/ListFlattener.java
@@ -0,0 +1,17 @@
+package bjc.utils.functypes;
+
+import java.util.function.Function;
+
+import bjc.utils.funcdata.IList;
+
+/**
+ * A function that flattens a list.
+ *
+ * @author bjculkin
+ *
+ * @param <S>
+ * The type of value in the list.
+ */
+public interface ListFlattener<S> extends Function<IList<S>, S> {
+
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java b/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java
new file mode 100644
index 0000000..a044bfd
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/CollectorUtils.java
@@ -0,0 +1,39 @@
+package bjc.utils.funcutils;
+
+import java.util.stream.Collector;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+
+/**
+ * Utilities for producing implementations of {@link Collector}
+ *
+ * @author ben
+ *
+ */
+public class CollectorUtils {
+ /**
+ * Create a collector that applies two collectors at once
+ *
+ * @param <InitialType>
+ * The type of the collection to collect from
+ * @param <AuxType1>
+ * The intermediate type of the first collector
+ * @param <AuxType2>
+ * The intermediate type of the second collector
+ * @param <FinalType1>
+ * The final type of the first collector
+ * @param <FinalType2>
+ * The final type of the second collector
+ * @param first
+ * The first collector to use
+ * @param second
+ * The second collector to use
+ * @return A collector that functions as mentioned above
+ */
+ public static <InitialType, AuxType1, AuxType2, FinalType1, FinalType2> Collector<InitialType, IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> compoundCollect(
+ final Collector<InitialType, AuxType1, FinalType1> first,
+ final Collector<InitialType, AuxType2, FinalType2> second) {
+ return new CompoundCollector<>(first, second);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java b/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java
new file mode 100644
index 0000000..35695bc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/CompoundCollector.java
@@ -0,0 +1,89 @@
+package bjc.utils.funcutils;
+
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.Pair;
+
+final class CompoundCollector<InitialType, AuxType1, AuxType2, FinalType1, FinalType2>
+ implements Collector<InitialType, IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> {
+
+ private final Set<java.util.stream.Collector.Characteristics> characteristicSet;
+
+ private final Collector<InitialType, AuxType1, FinalType1> first;
+ private final Collector<InitialType, AuxType2, FinalType2> second;
+
+ public CompoundCollector(final Collector<InitialType, AuxType1, FinalType1> first,
+ final Collector<InitialType, AuxType2, FinalType2> second) {
+ this.first = first;
+ this.second = second;
+
+ characteristicSet = first.characteristics();
+ characteristicSet.addAll(second.characteristics());
+ }
+
+ @Override
+ public BiConsumer<IHolder<IPair<AuxType1, AuxType2>>, InitialType> accumulator() {
+ final BiConsumer<AuxType1, InitialType> firstAccumulator = first.accumulator();
+ final BiConsumer<AuxType2, InitialType> secondAccumulator = second.accumulator();
+
+ return (state, value) -> {
+ state.doWith(statePair -> {
+ statePair.doWith((left, right) -> {
+ firstAccumulator.accept(left, value);
+ secondAccumulator.accept(right, value);
+ });
+ });
+ };
+ }
+
+ @Override
+ public Set<java.util.stream.Collector.Characteristics> characteristics() {
+ return characteristicSet;
+ }
+
+ @Override
+ public BinaryOperator<IHolder<IPair<AuxType1, AuxType2>>> combiner() {
+ final BinaryOperator<AuxType1> firstCombiner = first.combiner();
+ final BinaryOperator<AuxType2> secondCombiner = second.combiner();
+
+ return (leftState, rightState) -> {
+ return leftState.unwrap(leftPair -> {
+ return rightState.transform(rightPair -> {
+ return leftPair.combine(rightPair, firstCombiner, secondCombiner);
+ });
+ });
+ };
+ }
+
+ @Override
+ public Function<IHolder<IPair<AuxType1, AuxType2>>, IPair<FinalType1, FinalType2>> finisher() {
+ return state -> {
+ return state.unwrap(pair -> {
+ return pair.bind((left, right) -> {
+ final FinalType1 finalLeft = first.finisher().apply(left);
+ final FinalType2 finalRight = second.finisher().apply(right);
+
+ return new Pair<>(finalLeft, finalRight);
+ });
+ });
+ };
+ }
+
+ @Override
+ public Supplier<IHolder<IPair<AuxType1, AuxType2>>> supplier() {
+ return () -> {
+ final AuxType1 initialLeft = first.supplier().get();
+ final AuxType2 initialRight = second.supplier().get();
+
+ return new Identity<>(new Pair<>(initialLeft, initialRight));
+ };
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/EnumUtils.java b/base/src/main/java/bjc/utils/funcutils/EnumUtils.java
new file mode 100644
index 0000000..e4c0bda
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/EnumUtils.java
@@ -0,0 +1,63 @@
+package bjc.utils.funcutils;
+
+import java.util.Random;
+import java.util.function.Consumer;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Utility methods on enums
+ *
+ * @author ben
+ *
+ */
+public class EnumUtils {
+ /**
+ * Do an action for a random number of enum values
+ *
+ * @param <E>
+ * The type of the enum
+ * @param clasz
+ * The enum class
+ * @param nValues
+ * The number of values to execute the action on
+ * @param action
+ * The action to perform on random values
+ * @param rnd
+ * The source of randomness to use
+ */
+ public static <E extends Enum<E>> void doForValues(final Class<E> clasz, final int nValues,
+ final Consumer<E> action, final Random rnd) {
+ final E[] enumValues = clasz.getEnumConstants();
+
+ final IList<E> valueList = new FunctionalList<>(enumValues);
+
+ final int randomValueCount = enumValues.length - nValues;
+
+ for (int i = 0; i <= randomValueCount; i++) {
+ final E rDir = valueList.randItem(rnd::nextInt);
+
+ valueList.removeMatching(rDir);
+ }
+
+ valueList.forEach(action);
+ }
+
+ /**
+ * Get a random value from an enum
+ *
+ * @param <E>
+ * The type of the enum
+ * @param clasz
+ * The class of the enum
+ * @param rnd
+ * The random source to use
+ * @return A random value from the specified enum
+ */
+ public static <E extends Enum<E>> E getRandomValue(final Class<E> clasz, final Random rnd) {
+ final E[] enumValues = clasz.getEnumConstants();
+
+ return new FunctionalList<>(enumValues).randItem(rnd::nextInt);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/FileUtils.java b/base/src/main/java/bjc/utils/funcutils/FileUtils.java
new file mode 100644
index 0000000..87199b1
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/FileUtils.java
@@ -0,0 +1,40 @@
+package bjc.utils.funcutils;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.function.BiPredicate;
+
+/**
+ * Utilities for doing things with files
+ *
+ * @author ben
+ *
+ */
+public class FileUtils {
+ /**
+ * Traverse a directory recursively. This is a depth-first traversal
+ *
+ *
+ * @param root
+ * The directory to start the traversal at
+ * @param predicate
+ * The predicate to determine whether or not to traverse
+ * a directory
+ * @param action
+ * The action to invoke upon each file in the directory.
+ * Returning true means to continue the traversal,
+ * returning false stops it
+ * @throws IOException
+ * if the walk throws an exception
+ *
+ * TODO If it becomes necessary, write another overload
+ * for this with all the buttons and knobs from
+ * walkFileTree
+ */
+ public static void traverseDirectory(final Path root, final BiPredicate<Path, BasicFileAttributes> predicate,
+ final BiPredicate<Path, BasicFileAttributes> action) throws IOException {
+ Files.walkFileTree(root, new FunctionalFileVisitor(predicate, action));
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/FuncUtils.java b/base/src/main/java/bjc/utils/funcutils/FuncUtils.java
new file mode 100644
index 0000000..9950add
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/FuncUtils.java
@@ -0,0 +1,76 @@
+package bjc.utils.funcutils;
+
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * Utility things for functions
+ *
+ * @author ben
+ *
+ */
+public class FuncUtils {
+ /**
+ * Convert a binary function into a unary function that returns a
+ * function
+ *
+ * @param <A>
+ * The initial type of the function
+ * @param <B>
+ * The intermediate type of the function
+ * @param <C>
+ * The terminal type of the function
+ * @param func
+ * The function to transform
+ * @return The function transformed into a unary function returning a
+ * function
+ */
+ public static <A, B, C> Function<A, Function<B, C>> curry2(final BiFunction<A, B, C> func) {
+ return arg1 -> arg2 -> {
+ return func.apply(arg1, arg2);
+ };
+ }
+
+ /**
+ * Do the specified action the specified number of times
+ *
+ * @param nTimes
+ * The number of times to do the action
+ * @param cons
+ * The action to perform
+ */
+ public static void doTimes(final int nTimes, final Consumer<Integer> cons) {
+ for (int i = 0; i < nTimes; i++) {
+ cons.accept(i);
+ }
+ }
+
+ /**
+ * Return an operator that executes until it converges.
+ *
+ * @param op
+ * The operator to execute.
+ * @param maxTries
+ * The maximum amount of times to apply the function in an
+ * attempt to cause it to converge.
+ */
+ public static <T> UnaryOperator<T> converge(final UnaryOperator<T> op, final int maxTries) {
+ return (val) -> {
+ T newVal = op.apply(val);
+ T oldVal;
+
+ int tries = 0;
+
+ do {
+ oldVal = newVal;
+ newVal = op.apply(newVal);
+
+ tries += 1;
+ } while(!newVal.equals(oldVal) && tries < maxTries);
+
+ return newVal;
+ };
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java b/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java
new file mode 100644
index 0000000..db6c43b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/FunctionalFileVisitor.java
@@ -0,0 +1,36 @@
+package bjc.utils.funcutils;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.function.BiPredicate;
+
+/*
+ * Functional implementation of a file visitor.
+ */
+final class FunctionalFileVisitor extends SimpleFileVisitor<Path> {
+ private final BiPredicate<Path, BasicFileAttributes> predicate;
+ private final BiPredicate<Path, BasicFileAttributes> action;
+
+ public FunctionalFileVisitor(final BiPredicate<Path, BasicFileAttributes> predicate,
+ final BiPredicate<Path, BasicFileAttributes> action) {
+ this.predicate = predicate;
+ this.action = action;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException {
+ if (predicate.test(dir, attrs)) return FileVisitResult.CONTINUE;
+
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
+ if (action.test(file, attrs)) return FileVisitResult.CONTINUE;
+
+ return FileVisitResult.TERMINATE;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java b/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java
new file mode 100644
index 0000000..f3b2254
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/GroupPartIteration.java
@@ -0,0 +1,62 @@
+package bjc.utils.funcutils;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Implements a single group partitioning pass on a list
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of element in the list being partitioned
+ */
+final class GroupPartIteration<E> implements Consumer<E> {
+ private final IList<IList<E>> returnedList;
+
+ public IList<E> currentPartition;
+ private final IList<E> rejectedItems;
+
+ private int numberInCurrentPartition;
+ private final int numberPerPartition;
+
+ private final Function<E, Integer> elementCounter;
+
+ public GroupPartIteration(final IList<IList<E>> returned, final IList<E> rejects, final int nPerPart,
+ final Function<E, Integer> eleCount) {
+ this.returnedList = returned;
+ this.rejectedItems = rejects;
+ this.numberPerPartition = nPerPart;
+ this.elementCounter = eleCount;
+
+ this.currentPartition = new FunctionalList<>();
+ this.numberInCurrentPartition = 0;
+ }
+
+ @Override
+ public void accept(final E value) {
+ final boolean shouldStartPartition = numberInCurrentPartition >= numberPerPartition;
+
+ if (shouldStartPartition) {
+ returnedList.add(currentPartition);
+
+ currentPartition = new FunctionalList<>();
+ numberInCurrentPartition = 0;
+ } else {
+ final int currentElementCount = elementCounter.apply(value);
+
+ final boolean shouldReject = numberInCurrentPartition
+ + currentElementCount >= numberPerPartition;
+
+ if (shouldReject) {
+ rejectedItems.add(value);
+ } else {
+ currentPartition.add(value);
+ numberInCurrentPartition += currentElementCount;
+ }
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/IBuilder.java b/base/src/main/java/bjc/utils/funcutils/IBuilder.java
new file mode 100644
index 0000000..a96a4d6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/IBuilder.java
@@ -0,0 +1,31 @@
+package bjc.utils.funcutils;
+
+/**
+ * Generic interface for objects that implement the builder pattern
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of object being built
+ */
+public interface IBuilder<E> {
+ /**
+ * Build the object this builder is building
+ *
+ * @return The built object
+ * @throws IllegalStateException
+ * if the data in the builder cannot be built into its
+ * corresponding object at this point in time
+ */
+ public E build();
+
+ /**
+ * Reset the state of this builder to its initial state
+ *
+ * @throws UnsupportedOperationException
+ * if the builder doesn't support resetting its state
+ */
+ public default void reset() {
+ throw new UnsupportedOperationException("Builder doesn't support state resetting");
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/Isomorphism.java b/base/src/main/java/bjc/utils/funcutils/Isomorphism.java
new file mode 100644
index 0000000..2d3655e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/Isomorphism.java
@@ -0,0 +1,60 @@
+package bjc.utils.funcutils;
+
+import java.util.function.Function;
+
+/**
+ * A pair of functions to transform between a pair of types.
+ *
+ * @author bjculkin
+ *
+ * @param <S>
+ * The source type of the isomorphism.
+ *
+ * @param <D>
+ * The destination type of isomorphism.
+ *
+ */
+public class Isomorphism<S, D> {
+ private Function<S, D> toFunc;
+ private Function<D, S> fromFunc;
+
+ /**
+ * Create a new isomorphism.
+ *
+ * @param to
+ * The 'forward' function, from the source to the
+ * definition.
+ *
+ * @param from
+ * The 'backward' function, from the definition to the
+ * source.
+ */
+ public Isomorphism(Function<S, D> to, Function<D, S> from) {
+ toFunc = to;
+ fromFunc = from;
+ }
+
+ /**
+ * Apply the isomorphism forward.
+ *
+ * @param val
+ * The source value.
+ *
+ * @return The destination value.
+ */
+ public D to(S val) {
+ return toFunc.apply(val);
+ }
+
+ /**
+ * Apply the isomorphism backward.
+ *
+ * @param val
+ * The destination value.
+ *
+ * @return The source value.
+ */
+ public S from(D val) {
+ return fromFunc.apply(val);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/LambdaLock.java b/base/src/main/java/bjc/utils/funcutils/LambdaLock.java
new file mode 100644
index 0000000..62c5d32
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/LambdaLock.java
@@ -0,0 +1,105 @@
+package bjc.utils.funcutils;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Supplier;
+
+/**
+ * A wrapper around a {@link ReadWriteLock} to ensure that the lock is used
+ * properly.
+ *
+ * @author EVE
+ *
+ */
+public class LambdaLock {
+ private final Lock readLock;
+ private final Lock writeLock;
+
+ /**
+ * Create a new lambda-enabled lock around a new lock.
+ */
+ public LambdaLock() {
+ this(new ReentrantReadWriteLock());
+ }
+
+ /**
+ * Create a new lambda-enabled lock.
+ *
+ * @param lck
+ * The lock to wrap.
+ */
+ public LambdaLock(final ReadWriteLock lck) {
+ readLock = lck.readLock();
+ writeLock = lck.writeLock();
+ }
+
+ /**
+ * Execute an action with the read lock taken.
+ *
+ * @param supp
+ * The action to call.
+ *
+ * @return The result of the action.
+ */
+ public <T> T read(final Supplier<T> supp) {
+ readLock.lock();
+
+ try {
+ return supp.get();
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Execute an action with the write lock taken.
+ *
+ * @param supp
+ * The action to call.
+ *
+ * @return The result of the action.
+ */
+ public <T> T write(final Supplier<T> supp) {
+ writeLock.lock();
+
+ try {
+ return supp.get();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Execute an action with the read lock taken.
+ *
+ * @param action
+ * The action to call.
+ *
+ */
+ public void read(final Runnable action) {
+ readLock.lock();
+
+ try {
+ action.run();
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Execute an action with the write lock taken.
+ *
+ * @param action
+ * The action to call.
+ */
+ public void write(final Runnable action) {
+ writeLock.lock();
+
+ try {
+ action.run();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/funcutils/ListUtils.java b/base/src/main/java/bjc/utils/funcutils/ListUtils.java
new file mode 100644
index 0000000..c0daa1e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/ListUtils.java
@@ -0,0 +1,294 @@
+package bjc.utils.funcutils;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Utilities for manipulating FunctionalLists that don't belong in the class
+ * itself
+ *
+ * @author ben
+ *
+ */
+public class ListUtils {
+ private static final int MAX_NTRIESPART = 50;
+
+ /**
+ * Collapse a string of tokens into a single string without adding any
+ * spaces
+ *
+ * @param input
+ * The list of tokens to collapse
+ * @return The collapsed string of tokens
+ */
+ public static String collapseTokens(final IList<String> input) {
+ if (input == null) throw new NullPointerException("Input must not be null");
+
+ return collapseTokens(input, "");
+ }
+
+ /**
+ * Collapse a string of tokens into a single string, adding the desired
+ * separator after each token
+ *
+ * @param input
+ * The list of tokens to collapse
+ * @param seperator
+ * The separator to use for separating tokens
+ * @return The collapsed string of tokens
+ */
+ public static String collapseTokens(final IList<String> input, final String seperator) {
+ if (input == null)
+ throw new NullPointerException("Input must not be null");
+ else if (seperator == null) throw new NullPointerException("Seperator must not be null");
+
+ if (input.getSize() < 1)
+ return "";
+ else if (input.getSize() == 1)
+ return input.first();
+ else {
+ final StringBuilder state = new StringBuilder();
+
+ int i = 1;
+ for (final String itm : input.toIterable()) {
+ state.append(itm);
+
+ if (i != input.getSize()) {
+ state.append(seperator);
+ }
+
+ i += 1;
+ }
+
+ return state.toString();
+ }
+ }
+
+ /**
+ * Select a number of random items from the list without replacement
+ *
+ * @param <E>
+ * The type of items to select
+ * @param list
+ * The list to select from
+ * @param number
+ * The number of items to selet
+ * @param rng
+ * A function that creates a random number from 0 to the
+ * desired number
+ * @return A new list containing the desired number of items randomly
+ * selected from the specified list without replacement
+ */
+
+ public static <E> IList<E> drawWithoutReplacement(final IList<E> list, final int number,
+ final Function<Integer, Integer> rng) {
+ final IList<E> selected = new FunctionalList<>(new ArrayList<>(number));
+
+ final int total = list.getSize();
+
+ final Iterator<E> itr = list.toIterable().iterator();
+ E element = null;
+
+ for (final int index = 0; itr.hasNext(); element = itr.next()) {
+ /*
+ * n - m
+ */
+ final int winningChance = number - selected.getSize();
+
+ /*
+ * N - t
+ */
+ final int totalChance = total - (index - 1);
+
+ /*
+ * Probability of selecting the t+1'th element
+ */
+ if (NumberUtils.isProbable(winningChance, totalChance, rng)) {
+ selected.add(element);
+ }
+ }
+
+ return selected;
+ }
+
+ /**
+ * Select a number of random items from the list, with replacement
+ *
+ * @param <E>
+ * The type of items to select
+ * @param list
+ * The list to select from
+ * @param number
+ * The number of items to selet
+ * @param rng
+ * A function that creates a random number from 0 to the
+ * desired number
+ * @return A new list containing the desired number of items randomly
+ * selected from the specified list
+ */
+ public static <E> IList<E> drawWithReplacement(final IList<E> list, final int number,
+ final Function<Integer, Integer> rng) {
+ final IList<E> selected = new FunctionalList<>(new ArrayList<>(number));
+
+ for (int i = 0; i < number; i++) {
+ selected.add(list.randItem(rng));
+ }
+
+ return selected;
+ }
+
+ /**
+ * Partition a list into a list of lists, where each element can count
+ * for more than one element in a partition
+ *
+ * @param <E>
+ * The type of elements in the list to partition
+ *
+ * @param input
+ * The list to partition
+ * @param counter
+ * The function to determine the count for each element
+ * for
+ * @param partitionSize
+ * The number of elements to put in each partition
+ *
+ * @return A list partitioned according to the above rules
+ */
+ public static <E> IList<IList<E>> groupPartition(final IList<E> input, final Function<E, Integer> counter,
+ final int partitionSize) {
+ if (input == null)
+ throw new NullPointerException("Input list must not be null");
+ else if (counter == null)
+ throw new NullPointerException("Counter must not be null");
+ else if (partitionSize < 1 || partitionSize > input.getSize()) {
+ final String fmt = "%d is not a valid partition size. Must be between 1 and %d";
+ final String msg = String.format(fmt, partitionSize, input.getSize());
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ /*
+ * List that holds our results
+ */
+ final IList<IList<E>> returned = new FunctionalList<>();
+
+ /*
+ * List that holds elements rejected during current pass
+ */
+ final IList<E> rejected = new FunctionalList<>();
+
+ final GroupPartIteration<E> it = new GroupPartIteration<>(returned, rejected, partitionSize, counter);
+
+ /*
+ * Run up to a certain number of passes
+ */
+ for (int numberOfIterations = 0; numberOfIterations < MAX_NTRIESPART
+ && !rejected.isEmpty(); numberOfIterations++) {
+ input.forEach(it);
+
+ if (rejected.isEmpty()) {
+ /*
+ * Nothing was rejected, so we're done
+ */
+ return returned;
+ }
+ }
+
+
+ final String fmt = "Heuristic (more than %d iterations of partitioning) detected an unpartitionable list. (%s)\nThe following elements were not partitioned: %s\nCurrent group in formation: %s\nPreviously formed groups: %s\n";
+
+ final String msg = String.format(fmt, MAX_NTRIESPART, input.toString(), rejected.toString(), it.currentPartition.toString(), returned.toString());
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ * Merge the contents of a bunch of lists together into a single list
+ *
+ * @param <E>
+ * The type of value in this lists
+ * @param lists
+ * The values in the lists to merge
+ * @return A list containing all the elements of the lists
+ */
+ @SafeVarargs
+ public static <E> IList<E> mergeLists(final IList<E>... lists) {
+ final IList<E> returned = new FunctionalList<>();
+
+ for (final IList<E> list : lists) {
+ for (final E itm : list.toIterable()) {
+ returned.add(itm);
+ }
+ }
+
+ return returned;
+ }
+
+ /**
+ * Pad the provided list out to the desired size
+ *
+ * @param <E>
+ * The type of elements in the list
+ * @param list
+ * The list to pad out
+ * @param counter
+ * The function to count elements with
+ * @param size
+ * The desired size of the list
+ * @param padder
+ * The function to get elements to pad with
+ * @return The list, padded to the desired size
+ * @throws IllegalArgumentException
+ * if the list couldn't be padded to the desired size
+ */
+ public static <E> IList<E> padList(final IList<E> list, final Function<E, Integer> counter, final int size,
+ final Supplier<E> padder) {
+ int count = 0;
+
+ final IList<E> returned = new FunctionalList<>();
+
+ for (final E itm : list.toIterable()) {
+ count += counter.apply(itm);
+
+ returned.add(itm);
+ }
+
+ if (count % size != 0) {
+ /*
+ * We need to pad
+ */
+ int needed = count % size;
+ int threshold = 0;
+
+ while (needed > 0 && threshold <= MAX_NTRIESPART) {
+ final E val = padder.get();
+ final int newCount = counter.apply(val);
+
+ if (newCount <= needed) {
+ returned.add(val);
+
+ threshold = 0;
+
+ needed -= newCount;
+ } else {
+ threshold += 1;
+ }
+ }
+
+ if (threshold > MAX_NTRIESPART) {
+ final String fmt = "Heuristic (more than %d iterations of attempting to pad) detected an unpaddable list. (%s)\nPartially padded list: %S";
+
+ final String msg = String.format(fmt, MAX_NTRIESPART, list.toString(), returned.toString());
+
+ throw new IllegalArgumentException(msg);
+ }
+ }
+
+ return returned;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/NumberUtils.java b/base/src/main/java/bjc/utils/funcutils/NumberUtils.java
new file mode 100644
index 0000000..770d3a5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/NumberUtils.java
@@ -0,0 +1,69 @@
+package bjc.utils.funcutils;
+
+import java.util.function.Function;
+
+/**
+ * Utility functions for dealing with numbers
+ *
+ * @author ben
+ *
+ */
+public class NumberUtils {
+ /**
+ * Compute the falling factorial of a number
+ *
+ * @param value
+ * The number to compute
+ * @param power
+ * The power to do the falling factorial for
+ * @return The falling factorial of the number to the power
+ */
+ public static int fallingFactorial(final int value, final int power) {
+ if (power == 0)
+ return 1;
+ else if (power == 1)
+ return value;
+ else {
+ int result = 1;
+
+ for (int currentSub = 0; currentSub < power + 1; currentSub++) {
+ result *= value - currentSub;
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Evaluates a linear probability distribution
+ *
+ * @param winning
+ * The number of winning possibilities
+ * @param total
+ * The number of total possibilities
+ * @param rng
+ * The function to use to generate a random possibility
+ * @return Whether or not a random possibility was a winning one
+ */
+ public static boolean isProbable(final int winning, final int total, final Function<Integer, Integer> rng) {
+ return rng.apply(total) < winning;
+ }
+
+ /**
+ * Check if a number is in an inclusive range.
+ *
+ * @param min
+ * The minimum value of the range.
+ *
+ * @param max
+ * The maximum value of the range.
+ *
+ * @param i
+ * The number to check.
+ *
+ * @return Whether the number is in the range.
+ */
+ public static boolean between(final int min, final int max, final int i) {
+ return i >= min && i <= max;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/StringUtils.java b/base/src/main/java/bjc/utils/funcutils/StringUtils.java
new file mode 100644
index 0000000..62f78f5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/StringUtils.java
@@ -0,0 +1,196 @@
+package bjc.utils.funcutils;
+
+import java.util.Deque;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.ibm.icu.text.BreakIterator;
+
+/**
+ * Utility methods for operations on strings
+ *
+ * @author ben
+ *
+ */
+public class StringUtils {
+ /**
+ * Check if a string consists only of one or more matches of a regular
+ * expression
+ *
+ * @param input
+ * The string to check
+ * @param rRegex
+ * The regex to see if the string only contains matches
+ * of
+ * @return Whether or not the string consists only of multiple matches
+ * of the provided regex
+ */
+ public static boolean containsOnly(final String input, final String rRegex) {
+ if (input == null)
+ throw new NullPointerException("Input must not be null");
+ else if (rRegex == null) throw new NullPointerException("Regex must not be null");
+
+ /*
+ * This regular expression is fairly simple.
+ *
+ * First, we match the beginning of the string. Then, we start a
+ * non-capturing group whose contents are the passed in regex.
+ * That group is then matched one or more times and the pattern
+ * matches to the end of the string
+ */
+ return input.matches("\\A(?:" + rRegex + ")+\\Z");
+ }
+
+ /**
+ * Indent the string being built in a StringBuilder n levels
+ *
+ * @param builder
+ * The builder to indent in
+ * @param levels
+ * The number of levels to indent
+ */
+ public static void indentNLevels(final StringBuilder builder, final int levels) {
+ for (int i = 0; i < levels; i++) {
+ builder.append("\t");
+ }
+ }
+
+ /**
+ * Print out a deque with a special case for easily showing a deque is
+ * empty
+ *
+ * @param <ContainedType>
+ * The type in the deque
+ * @param queue
+ * The deque to print
+ * @return A string version of the deque, with allowance for an empty
+ * deque
+ */
+ public static <ContainedType> String printDeque(final Deque<ContainedType> queue) {
+ return queue.isEmpty() ? "(none)" : queue.toString();
+ }
+
+ /**
+ * Converts a sequence to an English list.
+ *
+ * @param objects
+ * The sequence to convert to an English list.
+ * @param join
+ * The string to use for separating the last element from
+ * the rest.
+ * @param comma
+ * The string to use as a comma
+ *
+ * @return The sequence as an English list.
+ */
+ public static String toEnglishList(final Object[] objects, final String join, final String comma) {
+ if (objects == null) throw new NullPointerException("Sequence must not be null");
+
+ final StringBuilder sb = new StringBuilder();
+
+ final String joiner = join;
+ final String coma = comma;
+
+ switch (objects.length) {
+ case 0:
+ /*
+ * Empty list.
+ */
+ break;
+ case 1:
+ /*
+ * One item.
+ */
+ sb.append(objects[0].toString());
+ break;
+ case 2:
+ /*
+ * Two items.
+ */
+ sb.append(objects[0].toString());
+ sb.append(" " + joiner + " ");
+ sb.append(objects[1].toString());
+ break;
+ default:
+ /*
+ * Three or more items.
+ */
+ for (int i = 0; i < objects.length - 1; i++) {
+ sb.append(objects[i].toString());
+ sb.append(coma + " ");
+ }
+ /*
+ * Uncomment this to remove serial commas.
+ *
+ * int lc = sb.length() - 1;
+ *
+ * sb.delete(lc - coma.length(), lc);
+ */
+ sb.append(joiner + " ");
+ sb.append(objects[objects.length - 1].toString());
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Converts a sequence to an English list.
+ *
+ * @param objects
+ * The sequence to convert to an English list.
+ * @param join
+ * The string to use for separating the last element from
+ * the rest.
+ *
+ * @return The sequence as an English list.
+ */
+ public static String toEnglishList(final Object[] objects, final String join) {
+ return toEnglishList(objects, join, ",");
+ }
+
+ /**
+ * Converts a sequence to an English list.
+ *
+ * @param objects
+ * The sequence to convert to an English list.
+ * @param and
+ * Whether to use 'and' or 'or'.
+ *
+ * @return The sequence as an English list.
+ */
+ public static String toEnglishList(final Object[] objects, final boolean and) {
+ if (and)
+ return toEnglishList(objects, "and");
+ else return toEnglishList(objects, "or");
+ }
+
+ /**
+ * Count the number of graphemes in a string.
+ *
+ * @param value
+ * The string to check.
+ *
+ * @return The number of graphemes in the string.
+ */
+ public static int graphemeCount(final String value) {
+ final BreakIterator it = BreakIterator.getCharacterInstance();
+ it.setText(value);
+
+ int count = 0;
+ while (it.next() != BreakIterator.DONE) {
+ count++;
+ }
+
+ return count;
+ }
+
+ public static int countMatches(final String value, final String pattern) {
+ Matcher mat = Pattern.compile(pattern).matcher(value);
+
+ int num = 0;
+ while(mat.find())
+ num += 1;
+
+ return num;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/TreeUtils.java b/base/src/main/java/bjc/utils/funcutils/TreeUtils.java
new file mode 100644
index 0000000..dcd5738
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/TreeUtils.java
@@ -0,0 +1,56 @@
+package bjc.utils.funcutils;
+
+import java.util.LinkedList;
+import java.util.function.Predicate;
+
+import bjc.utils.data.ITree;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Implements various utilities for trees.
+ *
+ * @author Benjamin Culkin
+ */
+public class TreeUtils {
+ /*
+ * Convert a tree into a list of outline nodes that match a certain
+ * path.
+ */
+ public static <T> IList<IList<T>> outlineTree(ITree<T> tre, Predicate<T> leafMarker) {
+ IList<IList<T>> paths = new FunctionalList<>();
+
+ LinkedList<T> path = new LinkedList<>();
+ path.add(tre.getHead());
+
+ tre.doForChildren((child) -> findPath(child, path, leafMarker, paths));
+
+ return paths;
+ }
+
+ private static <T> void findPath(ITree<T> subtree, LinkedList<T> path, Predicate<T> leafMarker, IList<IList<T>> paths) {
+ if(subtree.getChildrenCount() == 0 && leafMarker.test(subtree.getHead())) {
+ /*
+ * We're at a matching leaf node. Add it.
+ */
+ IList<T> finalPath = new FunctionalList<>();
+
+ for(T ePath : path) {
+ finalPath.add(ePath);
+ }
+
+ finalPath.add(subtree.getHead());
+
+ paths.add(finalPath);
+ } else {
+ /*
+ * Check the children of this node.
+ */
+ path.add(subtree.getHead());
+
+ subtree.doForChildren((child) -> findPath(child, path, leafMarker, paths));
+
+ path.removeLast();
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/TriConsumer.java b/base/src/main/java/bjc/utils/funcutils/TriConsumer.java
new file mode 100644
index 0000000..f30386c
--- /dev/null
+++ b/base/src/main/java/bjc/utils/funcutils/TriConsumer.java
@@ -0,0 +1,31 @@
+package bjc.utils.funcutils;
+
+/**
+ * Consumer that takes three arguments.
+ *
+ * @author EVE
+ *
+ * @param <A>
+ * Type of the first argument.
+ * @param <B>
+ * Type of the second argument.
+ * @param <C>
+ * Type of the third argument.
+ *
+ */
+@FunctionalInterface
+public interface TriConsumer<A, B, C> {
+ /**
+ * Perform the action.
+ *
+ * @param a
+ * The first parameter.
+ *
+ * @param b
+ * The second parameter.
+ *
+ * @param c
+ * The third parameter.
+ */
+ public void accept(A a, B b, C c);
+}
diff --git a/base/src/main/java/bjc/utils/gen/RandomGrammar.java b/base/src/main/java/bjc/utils/gen/RandomGrammar.java
new file mode 100644
index 0000000..3de08d6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gen/RandomGrammar.java
@@ -0,0 +1,69 @@
+package bjc.utils.gen;
+
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A weighted grammar where all the rules have a equal chance of occuring.
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of grammar elements to use.
+ */
+public class RandomGrammar<E> extends WeightedGrammar<E> {
+ /**
+ * Create a new random grammar.
+ */
+ public RandomGrammar() {
+ rules = new FunctionalMap<>();
+ }
+
+ /**
+ * Add cases to a specified rule.
+ *
+ * @param rule
+ * The name of the rule to add cases to.
+ * @param cases
+ * The cases to add for this rule.
+ */
+ @SafeVarargs
+ public final void addCases(final E rule, final IList<E>... cases) {
+ for (final IList<E> currentCase : cases) {
+ super.addCase(rule, 1, currentCase);
+ }
+ }
+
+ /**
+ * Create a rule with the specified name and cases.
+ *
+ * @param rule
+ * The name of the rule to add.
+ * @param cases
+ * The cases to add for this rule.
+ */
+ @SafeVarargs
+ public final void makeRule(final E rule, final IList<E>... cases) {
+ super.addRule(rule);
+
+ for (final IList<E> currentCase : cases) {
+ super.addCase(rule, 1, currentCase);
+ }
+ }
+
+ /**
+ * Create a rule with the specified name and cases.
+ *
+ * @param rule
+ * The name of the rule to add.
+ * @param cases
+ * The cases to add for this rule.
+ */
+ public void makeRule(final E rule, final IList<IList<E>> cases) {
+ if (cases == null) throw new NullPointerException("Cases must not be null");
+
+ super.addRule(rule);
+
+ cases.forEach(currentCase -> super.addCase(rule, 1, currentCase));
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gen/WeightedGrammar.java b/base/src/main/java/bjc/utils/gen/WeightedGrammar.java
new file mode 100644
index 0000000..7777ad8
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gen/WeightedGrammar.java
@@ -0,0 +1,573 @@
+package bjc.utils.gen;
+
+import java.util.Random;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import bjc.utils.data.IPair;
+import bjc.utils.data.Pair;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A random grammar, where certain rules will come up more often than others.
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The values that make up sentences of this grammar.
+ */
+public class WeightedGrammar<E> {
+ /**
+ * The initial rule of the grammar
+ */
+ protected String initialRule;
+
+ /**
+ * The rules currently in this grammar
+ */
+ protected IMap<E, WeightedRandom<IList<E>>> rules;
+
+ /**
+ * The random number generator used for random numbers
+ */
+ private Random rng;
+
+ /**
+ * All of the subgrammars of this grammar
+ */
+ protected IMap<E, WeightedGrammar<E>> subgrammars;
+
+ /**
+ * Rules that require special handling
+ */
+ private IMap<E, Supplier<IList<E>>> specialRules;
+
+ /**
+ * Predicate for marking special tokens
+ */
+
+ private Predicate<E> specialMarker;
+
+ /**
+ * Action for special tokens
+ */
+ private BiFunction<E, WeightedGrammar<E>, IList<E>> specialAction;
+
+ /**
+ * Create a new weighted grammar.
+ */
+ public WeightedGrammar() {
+ rules = new FunctionalMap<>();
+ subgrammars = new FunctionalMap<>();
+ specialRules = new FunctionalMap<>();
+ }
+
+ /**
+ * Create a new weighted grammar that uses the specified source of
+ * randomness.
+ *
+ * @param source
+ * The source of randomness to use
+ */
+ public WeightedGrammar(final Random source) {
+ this();
+
+ if (source == null) throw new NullPointerException("Source of randomness must be non-null");
+
+ rng = source;
+ }
+
+ /**
+ * Configure the action to perform on special tokens.
+ *
+ * @param marker
+ * The marker to find special tokens.
+ *
+ * @param action
+ * The action to take on those tokens.
+ */
+ public void configureSpecial(final Predicate<E> marker,
+ final BiFunction<E, WeightedGrammar<E>, IList<E>> action) {
+ specialMarker = marker;
+ specialAction = action;
+ }
+
+ /**
+ * Adds a special rule to the grammar.
+ *
+ * @param ruleName
+ * The name of the special rule.
+ *
+ * @param cse
+ * The case for the rule.
+ */
+ public void addSpecialRule(final E ruleName, final Supplier<IList<E>> cse) {
+ if (ruleName == null)
+ throw new NullPointerException("Rule name must not be null");
+ else if (cse == null) throw new NullPointerException("Case must not be null");
+
+ specialRules.put(ruleName, cse);
+ }
+
+ /**
+ * Add a case to an already existing rule.
+ *
+ * @param ruleName
+ * The rule to add a case to.
+ * @param probability
+ * The probability for this rule to be chosen.
+ * @param cse
+ * The case being added.
+ */
+ public void addCase(final E ruleName, final int probability, final IList<E> cse) {
+ if (ruleName == null)
+ throw new NullPointerException("Rule name must be not null");
+ else if (cse == null) throw new NullPointerException("Case body must not be null");
+
+ rules.get(ruleName).addProbability(probability, cse);
+ }
+
+ /**
+ * Add a alias for an existing subgrammar
+ *
+ * @param name
+ * The name of the subgrammar to alias
+ * @param alias
+ * The alias of the subgrammar
+ * @return Whether the alias was succesfully created
+ */
+ public boolean addGrammarAlias(final E name, final E alias) {
+ if (name == null)
+ throw new NullPointerException("Subgrammar name must not be null");
+ else if (alias == null) throw new NullPointerException("Subgrammar alias must not be null");
+
+ if (subgrammars.containsKey(alias)) return false;
+
+ if (subgrammars.containsKey(name)) {
+ subgrammars.put(alias, subgrammars.get(name));
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Add a new rule with no cases.
+ *
+ * @param name
+ * The name of the rule to add.
+ * @return Whether or not the rule was successfully added.
+ */
+ public boolean addRule(final E name) {
+ if (rng == null) {
+ rng = new Random();
+ }
+
+ if (name == null) throw new NullPointerException("Rule name must not be null");
+
+ return addRule(name, new WeightedRandom<>(rng));
+ }
+
+ /**
+ * Add a new rule with a set of cases.
+ *
+ * @param name
+ * The name of the rule to add.
+ * @param cases
+ * The set of cases for the rule.
+ * @return Whether or not the rule was succesfully added.
+ */
+ public boolean addRule(final E name, final WeightedRandom<IList<E>> cases) {
+ if (name == null)
+ throw new NullPointerException("Name must not be null");
+ else if (cases == null) throw new NullPointerException("Cases must not be null");
+
+ if (rules.containsKey(name)) return false;
+
+ rules.put(name, cases);
+ return true;
+ }
+
+ /**
+ * Add a subgrammar.
+ *
+ * @param name
+ * The name of the subgrammar.
+ * @param subgrammar
+ * The subgrammar to add.
+ * @return Whether or not the subgrammar was succesfully added.
+ */
+ public boolean addSubgrammar(final E name, final WeightedGrammar<E> subgrammar) {
+ if (name == null)
+ throw new NullPointerException("Subgrammar name must not be null");
+ else if (subgrammar == null) throw new NullPointerException("Subgrammar must not be null");
+
+ if (subgrammars.containsKey(name)) return false;
+
+ subgrammars.put(name, subgrammar);
+ return true;
+ }
+
+ /**
+ * Remove a rule with the specified name.
+ *
+ * @param name
+ * The name of the rule to remove.
+ */
+ public void deleteRule(final E name) {
+ if (name == null) throw new NullPointerException("Rule name must not be null");
+
+ rules.remove(name);
+ }
+
+ /**
+ * Remove a subgrammar with the specified name.
+ *
+ * @param name
+ * The name of the subgrammar to remove.
+ */
+ public void deleteSubgrammar(final E name) {
+ if (name == null) throw new NullPointerException("Rule name must not be null");
+
+ subgrammars.remove(name);
+ }
+
+ /**
+ * Generate a set of debug sentences for the specified rule.
+ *
+ * Only generates sentences one layer deep.
+ *
+ * @param ruleName
+ * The rule to test.
+ * @return A set of sentences generated by the specified rule.
+ */
+ public IList<IList<E>> generateDebugValues(final E ruleName) {
+ if (ruleName == null) throw new NullPointerException("Rule name must not be null");
+
+ final IList<IList<E>> returnedList = new FunctionalList<>();
+
+ final WeightedRandom<IList<E>> ruleGenerator = rules.get(ruleName);
+
+ for (int i = 0; i < 10; i++) {
+ returnedList.add(ruleGenerator.generateValue());
+ }
+
+ return returnedList;
+ }
+
+ /**
+ * Generate a generic sentence from a initial rule.
+ *
+ * @param <T>
+ * The type of the transformed output
+ *
+ * @param initRules
+ * The initial rule to start with.
+ *
+ * @param tokenTransformer
+ * The function to transform grammar output into
+ * something.
+ *
+ * @param spacer
+ * The spacer element to add in between output tokens.
+ *
+ * @return A randomly generated sentence from the specified initial
+ * rule.
+ */
+ public <T> IList<T> generateGenericValues(final E initRules, final Function<E, T> tokenTransformer,
+ final T spacer) {
+ if (initRules == null)
+ throw new NullPointerException("Initial rule must not be null");
+ else if (tokenTransformer == null)
+ throw new NullPointerException("Transformer must not be null");
+ else if (spacer == null) throw new NullPointerException("Spacer must not be null");
+
+ final IList<T> returnedList = new FunctionalList<>();
+
+ IList<E> genRules = new FunctionalList<>(initRules);
+
+ if (specialMarker != null) {
+ if (specialMarker.test(initRules)) {
+ genRules = specialAction.apply(initRules, this);
+ }
+ }
+
+ for (final E initRule : genRules.toIterable()) {
+ if (specialRules.containsKey(initRule)) {
+ for (final E rulePart : specialRules.get(initRule).get().toIterable()) {
+ final Iterable<T> generatedRuleParts = generateGenericValues(rulePart,
+ tokenTransformer, spacer).toIterable();
+
+ for (final T generatedRulePart : generatedRuleParts) {
+ returnedList.add(generatedRulePart);
+ returnedList.add(spacer);
+ }
+ }
+ } else if (subgrammars.containsKey(initRule)) {
+ final Iterable<T> ruleParts = subgrammars.get(initRule)
+ .generateGenericValues(initRule, tokenTransformer, spacer).toIterable();
+
+ for (final T rulePart : ruleParts) {
+ returnedList.add(rulePart);
+ returnedList.add(spacer);
+ }
+ } else if (rules.containsKey(initRule)) {
+ final Iterable<E> ruleParts = rules.get(initRule).generateValue().toIterable();
+
+ for (final E rulePart : ruleParts) {
+ final Iterable<T> generatedRuleParts = generateGenericValues(rulePart,
+ tokenTransformer, spacer).toIterable();
+
+ for (final T generatedRulePart : generatedRuleParts) {
+ returnedList.add(generatedRulePart);
+ returnedList.add(spacer);
+ }
+ }
+ } else {
+ final T transformedToken = tokenTransformer.apply(initRule);
+
+ if (transformedToken == null)
+ throw new NullPointerException("Transformer created null token");
+
+ returnedList.add(transformedToken);
+ returnedList.add(spacer);
+ }
+ }
+
+ return returnedList;
+ }
+
+ /**
+ * Generate a random list of grammar elements from a given initial rule.
+ *
+ * @param initRule
+ * The initial rule to start with.
+ * @param spacer
+ * The item to use to space the list.
+ * @return A list of random grammar elements generated by the specified
+ * rule.
+ */
+ public IList<E> generateListValues(final E initRule, final E spacer) {
+ final IList<E> retList = generateGenericValues(initRule, strang -> strang, spacer);
+
+ return retList;
+ }
+
+ /**
+ * Get the initial rule of this grammar
+ *
+ * @return The initial rule of this grammar
+ */
+ public String getInitialRule() {
+ return initialRule;
+ }
+
+ /**
+ * Returns the number of rules in this grammar
+ *
+ * @return The number of rules in this grammar
+ */
+ public int getRuleCount() {
+ return rules.size();
+ }
+
+ /**
+ * Returns a set containing all of the rules in this grammar
+ *
+ * @return The set of all rule names in this grammar
+ */
+ public IList<E> getRuleNames() {
+ final IList<E> ruleNames = new FunctionalList<>();
+
+ ruleNames.addAll(rules.keyList());
+ ruleNames.addAll(specialRules.keyList());
+
+ return ruleNames;
+ }
+
+ /**
+ * Get the subgrammar with the specified name.
+ *
+ * @param name
+ * The name of the subgrammar to get.
+ * @return The subgrammar with the specified name.
+ */
+ public WeightedGrammar<E> getSubgrammar(final E name) {
+ if (name == null) throw new NullPointerException("Subgrammar name must not be null");
+
+ return subgrammars.get(name);
+ }
+
+ /**
+ * Check if this grammar has an initial rule
+ *
+ * @return Whether or not this grammar has an initial rule
+ */
+ public boolean hasInitialRule() {
+ return initialRule != null && !initialRule.equalsIgnoreCase("");
+ }
+
+ /**
+ * Check if this grammar has a given rule.
+ *
+ * @param ruleName
+ * The rule to check for.
+ *
+ * @return Whether or not the grammar has a rule by that name.
+ */
+ public boolean hasRule(final E ruleName) {
+ return rules.containsKey(ruleName) || specialRules.containsKey(ruleName);
+ }
+
+ /**
+ * Prefix a given rule with a token multiple times
+ *
+ * @param ruleName
+ * The name of the rule to prefix
+ * @param prefixToken
+ * The token to prefix to the rules
+ * @param additionalProbability
+ * The additional probability of the tokens
+ * @param numberOfTimes
+ * The number of times to prefix the token
+ */
+ public void multiPrefixRule(final E ruleName, final E prefixToken, final int additionalProbability,
+ final int numberOfTimes) {
+ if (ruleName == null)
+ throw new NullPointerException("Rule name must not be null");
+ else if (prefixToken == null)
+ throw new NullPointerException("Prefix token must not be null");
+ else if (numberOfTimes < 1)
+ throw new IllegalArgumentException("Number of times to prefix must be positive.");
+
+ final WeightedRandom<IList<E>> rule = rules.get(ruleName);
+
+ final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>();
+
+ rule.getValues().forEach((pair) -> {
+ final IList<IList<E>> newRule = new FunctionalList<>();
+
+ for (int i = 1; i <= numberOfTimes; i++) {
+ final IList<E> newCase = pair.merge((left, right) -> {
+ final IList<E> returnVal = new FunctionalList<>();
+
+ for (final E val : right.toIterable()) {
+ returnVal.add(val);
+ }
+
+ return returnVal;
+ });
+
+ for (int j = 1; j <= i; j++) {
+ newCase.prepend(prefixToken);
+ }
+
+ newRule.add(newCase);
+ }
+
+ newRule.forEach((list) -> {
+ final Integer currentProb = pair.merge((left, right) -> left);
+
+ newResults.add(new Pair<>(currentProb + additionalProbability, list));
+ });
+ });
+
+ newResults.forEach((pair) -> {
+ pair.doWith((left, right) -> {
+ addCase(ruleName, left, right);
+ });
+ });
+ }
+
+ /**
+ * Create a series of alternatives for a rule by prefixing them with a
+ * given token
+ *
+ * @param additionalProbability
+ * The amount to adjust the probability by
+ * @param ruleName
+ * The name of the rule to prefix
+ * @param prefixToken
+ * The token to prefix to the rule
+ */
+ public void prefixRule(final E ruleName, final E prefixToken, final int additionalProbability) {
+ if (ruleName == null)
+ throw new NullPointerException("Rule name must not be null");
+ else if (prefixToken == null) throw new NullPointerException("Prefix token must not be null");
+
+ final WeightedRandom<IList<E>> rule = rules.get(ruleName);
+
+ final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>();
+
+ rule.getValues().forEach((pair) -> {
+ final IList<E> newCase = pair.merge((left, right) -> {
+ final IList<E> returnVal = new FunctionalList<>();
+
+ for (final E val : right.toIterable()) {
+ returnVal.add(val);
+ }
+
+ return returnVal;
+ });
+
+ newCase.prepend(prefixToken);
+
+ newResults.add(new Pair<>(pair.merge((left, right) -> left) + additionalProbability, newCase));
+ });
+
+ newResults.forEach((pair) -> pair.doWith((left, right) -> addCase(ruleName, left, right)));
+ }
+
+ /**
+ * Set the initial rule of the graphic
+ *
+ * @param initRule
+ * The initial rule of this grammar
+ */
+ public void setInitialRule(final String initRule) {
+ this.initialRule = initRule;
+ }
+
+ /**
+ * Suffix a token to a rule
+ *
+ * @param ruleName
+ * The rule to suffix
+ * @param suffixToken
+ * The token to prefix to the rule
+ * @param additionalProbability
+ * Additional probability of the prefixed rule
+ */
+ public void suffixRule(final E ruleName, final E suffixToken, final int additionalProbability) {
+ if (ruleName == null)
+ throw new NullPointerException("Rule name must not be null");
+ else if (suffixToken == null) throw new NullPointerException("Prefix token must not be null");
+
+ final WeightedRandom<IList<E>> rule = rules.get(ruleName);
+
+ final IList<IPair<Integer, IList<E>>> newResults = new FunctionalList<>();
+
+ rule.getValues().forEach((par) -> {
+ final IList<E> newCase = par.merge((left, right) -> {
+ final IList<E> returnVal = new FunctionalList<>();
+
+ for (final E val : right.toIterable()) {
+ returnVal.add(val);
+ }
+
+ return returnVal;
+ });
+
+ newCase.add(suffixToken);
+
+ newResults.add(new Pair<>(par.merge((left, right) -> left) + additionalProbability, newCase));
+ });
+
+ newResults.forEach((pair) -> pair.doWith((left, right) -> addCase(ruleName, left, right)));
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gen/WeightedRandom.java b/base/src/main/java/bjc/utils/gen/WeightedRandom.java
new file mode 100644
index 0000000..18225ef
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gen/WeightedRandom.java
@@ -0,0 +1,112 @@
+package bjc.utils.gen;
+
+import java.util.Random;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Represents a random number generator where certain results are weighted more
+ * heavily than others.
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of values that are randomly selected.
+ */
+public class WeightedRandom<E> {
+ /*
+ * The list of probabilities for each result
+ */
+ private final IList<Integer> probabilities;
+
+ /*
+ * The list of possible results to pick from
+ */
+ private final IList<E> results;
+
+ /*
+ * The source for any needed random numbers
+ */
+ private final Random source;
+
+ private int totalChance;
+
+ /**
+ * Create a new weighted random generator with the specified source of
+ * randomness
+ *
+ * @param src
+ * The source of randomness to use.
+ */
+ public WeightedRandom(final Random src) {
+ probabilities = new FunctionalList<>();
+ results = new FunctionalList<>();
+
+ if (src == null) throw new NullPointerException("Source of randomness must not be null");
+
+ source = src;
+ }
+
+ /**
+ * Add a probability for a specific result to be given.
+ *
+ * @param chance
+ * The chance to get this result.
+ * @param result
+ * The result to get when the chance comes up.
+ */
+ public void addProbability(final int chance, final E result) {
+ probabilities.add(chance);
+ results.add(result);
+
+ totalChance += chance;
+ }
+
+ /**
+ * Generate a weighted random value.
+ *
+ * @return A random value selected in a weighted fashion.
+ */
+ public E generateValue() {
+ final IHolder<Integer> value = new Identity<>(source.nextInt(totalChance));
+ final IHolder<E> current = new Identity<>();
+ final IHolder<Boolean> picked = new Identity<>(true);
+
+ probabilities.forEachIndexed((index, probability) -> {
+ if (picked.unwrap(bool -> bool)) {
+ if (value.unwrap((number) -> number < probability)) {
+ current.transform((result) -> results.getByIndex(index));
+
+ picked.transform((bool) -> false);
+ } else {
+ value.transform((number) -> number - probability);
+ }
+ }
+ });
+
+ return current.unwrap((result) -> result);
+ }
+
+ /**
+ * Return a list of values that can be generated by this generator
+ *
+ * @return A list of all the values that can be generated
+ */
+ public IList<E> getResults() {
+ return results;
+ }
+
+ /**
+ * Return a list containing values that can be generated paired with the
+ * probability of those values being generated
+ *
+ * @return A list of pairs of values and value probabilities
+ */
+ public IList<IPair<Integer, E>> getValues() {
+ return probabilities.pairWith(results);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/graph/AdjacencyMap.java b/base/src/main/java/bjc/utils/graph/AdjacencyMap.java
new file mode 100644
index 0000000..446ab5b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/graph/AdjacencyMap.java
@@ -0,0 +1,216 @@
+package bjc.utils.graph;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.util.InputMismatchException;
+import java.util.Scanner;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Identity;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+import bjc.utils.funcutils.FuncUtils;
+
+/**
+ * An adjacency map representing a graph
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The type of the nodes in the graph
+ */
+public class AdjacencyMap<T> {
+ /**
+ * Create an adjacency map from a stream of text
+ *
+ * @param stream
+ * The stream of text to read in
+ * @return An adjacency map defined by the text
+ */
+ public static AdjacencyMap<Integer> fromStream(final InputStream stream) {
+ if (stream == null) throw new NullPointerException("Input source must not be null");
+
+ // Create the adjacency map
+ AdjacencyMap<Integer> adjacency;
+
+ try (Scanner input = new Scanner(stream)) {
+ input.useDelimiter("\n");
+
+ int vertexCount;
+
+ final String possible = input.next();
+
+ try {
+ // First, read in number of vertices
+ vertexCount = Integer.parseInt(possible);
+ } catch (final NumberFormatException nfex) {
+ final InputMismatchException imex = new InputMismatchException(
+ "The first line must contain the number of vertices. " + possible
+ + " is not a valid number");
+
+ imex.initCause(nfex);
+
+ throw imex;
+ }
+
+ if (vertexCount <= 0)
+ throw new InputMismatchException("The number of vertices must be greater than 0");
+
+ final IList<Integer> vertices = new FunctionalList<>();
+
+ FuncUtils.doTimes(vertexCount, (vertexNo) -> vertices.add(vertexNo));
+
+ adjacency = new AdjacencyMap<>(vertices);
+
+ final IHolder<Integer> row = new Identity<>(0);
+
+ input.forEachRemaining((strang) -> {
+ readRow(adjacency, vertexCount, row, strang);
+ });
+ }
+
+ return adjacency;
+ }
+
+ private static void readRow(final AdjacencyMap<Integer> adjacency, final int vertexCount,
+ final IHolder<Integer> row, final String strang) {
+ final String[] parts = strang.split(" ");
+
+ if (parts.length != vertexCount)
+ throw new InputMismatchException("Must specify a weight for all " + vertexCount + " vertices");
+
+ int column = 0;
+
+ for (final String part : parts) {
+ int weight;
+
+ try {
+ weight = Integer.parseInt(part);
+ } catch (final NumberFormatException nfex) {
+ final InputMismatchException imex = new InputMismatchException(
+ "" + part + " is not a valid weight.");
+
+ imex.initCause(nfex);
+
+ throw imex;
+ }
+
+ adjacency.setWeight(row.getValue(), column, weight);
+
+ column++;
+ }
+
+ row.transform((rowNumber) -> rowNumber + 1);
+ }
+
+ /**
+ * The backing storage of the map
+ */
+ private final IMap<T, IMap<T, Integer>> adjacency = new FunctionalMap<>();
+
+ /**
+ * Create a new map from a set of vertices
+ *
+ * @param vertices
+ * The set of vertices to create a map from
+ */
+ public AdjacencyMap(final IList<T> vertices) {
+ if (vertices == null) throw new NullPointerException("Vertices must not be null");
+
+ vertices.forEach(vertex -> {
+ final IMap<T, Integer> row = new FunctionalMap<>();
+
+ vertices.forEach(target -> {
+ row.put(target, 0);
+ });
+
+ adjacency.put(vertex, row);
+ });
+ }
+
+ /**
+ * Check if the graph is directed
+ *
+ * @return Whether or not the graph is directed
+ */
+ public boolean isDirected() {
+ final IHolder<Boolean> result = new Identity<>(true);
+
+ adjacency.forEach((sourceKey, sourceValue) -> {
+ sourceValue.forEach((targetKey, targetValue) -> {
+ final int inverseValue = adjacency.get(targetKey).get(sourceKey);
+
+ if (targetValue != inverseValue) {
+ result.replace(false);
+ }
+ });
+ });
+
+ return result.getValue();
+ }
+
+ /**
+ * Set the weight of an edge
+ *
+ * @param source
+ * The source node of the edge
+ * @param target
+ * The target node of the edge
+ * @param weight
+ * The weight of the edge
+ */
+ public void setWeight(final T source, final T target, final int weight) {
+ if (source == null)
+ throw new NullPointerException("Source vertex must not be null");
+ else if (target == null) throw new NullPointerException("Target vertex must not be null");
+
+ if (!adjacency.containsKey(source))
+ throw new IllegalArgumentException("Source vertex " + source + " isn't present in map");
+ else if (!adjacency.containsKey(target))
+ throw new IllegalArgumentException("Target vertex " + target + " isn't present in map");
+
+ adjacency.get(source).put(target, weight);
+ }
+
+ /**
+ * Convert this to a different graph representation
+ *
+ * @return The new representation of this graph
+ */
+ public Graph<T> toGraph() {
+ final Graph<T> ret = new Graph<>();
+
+ adjacency.forEach((sourceKey, sourceValue) -> {
+ sourceValue.forEach((targetKey, targetValue) -> {
+ ret.addEdge(sourceKey, targetKey, targetValue, true);
+ });
+ });
+
+ return ret;
+ }
+
+ /**
+ * Convert an adjacency map back into a stream
+ *
+ * @param sink
+ * The stream to convert to
+ */
+ public void toStream(final OutputStream sink) {
+ if (sink == null) throw new NullPointerException("Output source must not be null");
+
+ final PrintStream outputPrinter = new PrintStream(sink);
+
+ adjacency.forEach((sourceKey, sourceValue) -> {
+ sourceValue.forEach((targetKey, targetValue) -> {
+ outputPrinter.printf("%d", targetValue);
+ });
+
+ outputPrinter.println();
+ });
+
+ outputPrinter.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/graph/Edge.java b/base/src/main/java/bjc/utils/graph/Edge.java
new file mode 100644
index 0000000..0152e3d
--- /dev/null
+++ b/base/src/main/java/bjc/utils/graph/Edge.java
@@ -0,0 +1,112 @@
+package bjc.utils.graph;
+
+/**
+ * An edge in a weighted graph
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The type of the nodes in the graph
+ */
+public class Edge<T> {
+ /*
+ * The distance from initial to terminal node
+ */
+ private final int distance;
+
+ /*
+ * The initial and terminal nodes of this edge
+ */
+ private final T source, target;
+
+ /**
+ * Create a new edge with set parameters
+ *
+ * @param initial
+ * The initial node of the edge
+ * @param terminal
+ * The terminal node of the edge
+ * @param distance
+ * The distance between initial and terminal edge
+ */
+ public Edge(final T initial, final T terminal, final int distance) {
+ if (initial == null)
+ throw new NullPointerException("Initial node must not be null");
+ else if (terminal == null) throw new NullPointerException("Terminal node must not be null");
+
+ this.source = initial;
+ this.target = terminal;
+ this.distance = distance;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj)
+ return true;
+ else if (obj == null)
+ return false;
+ else if (getClass() != obj.getClass())
+ return false;
+ else {
+ final Edge<?> other = (Edge<?>) obj;
+
+ if (distance != other.distance)
+ return false;
+ else if (source == null) {
+ if (other.source != null) return false;
+ } else if (!source.equals(other.source))
+ return false;
+ else if (target == null) {
+ if (other.target != null) return false;
+ } else if (!target.equals(other.target)) return false;
+
+ return true;
+ }
+ }
+
+ /**
+ * Get the distance in this edge
+ *
+ * @return The distance between the initial and terminal nodes of this
+ * edge
+ */
+ public int getDistance() {
+ return distance;
+ }
+
+ /**
+ * Get the initial node of an edge
+ *
+ * @return The initial node of this edge
+ */
+ public T getSource() {
+ return source;
+ }
+
+ /**
+ * Get the target node of an edge
+ *
+ * @return The target node of this edge
+ */
+ public T getTarget() {
+ return target;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+
+ int result = 1;
+
+ result = prime * result + distance;
+ result = prime * result + (source == null ? 0 : source.hashCode());
+ result = prime * result + (target == null ? 0 : target.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return " first vertex " + source + " to vertex " + target + " with distance: " + distance;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/graph/Graph.java b/base/src/main/java/bjc/utils/graph/Graph.java
new file mode 100644
index 0000000..280a7f5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/graph/Graph.java
@@ -0,0 +1,267 @@
+package bjc.utils.graph;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiPredicate;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.Identity;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * A directed weighted graph, where the vertices have some arbitrary label
+ *
+ * @author ben
+ *
+ * @param <T>
+ * The label for vertices
+ */
+public class Graph<T> {
+ /**
+ * Create a graph from a list of edges
+ *
+ * @param <E>
+ * The type of data stored in the edges
+ *
+ * @param edges
+ * The list of edges to build from
+ * @return A graph built from the provided edge-list
+ */
+ public static <E> Graph<E> fromEdgeList(final List<Edge<E>> edges) {
+ final Graph<E> g = new Graph<>();
+
+ edges.forEach(edge -> {
+ g.addEdge(edge.getSource(), edge.getTarget(), edge.getDistance(), true);
+ });
+
+ return g;
+ }
+
+ /**
+ * The backing representation of the graph
+ */
+ private final IMap<T, IMap<T, Integer>> backing;
+
+ /**
+ * Create a new graph
+ */
+ public Graph() {
+ backing = new FunctionalMap<>();
+ }
+
+ /**
+ * Add a edge to the graph
+ *
+ * @param source
+ * The source vertex for this edge
+ * @param target
+ * The target vertex for this edge
+ * @param distance
+ * The distance from the source vertex to the target
+ * vertex
+ * @param directed
+ * Whether or not
+ */
+ public void addEdge(final T source, final T target, final int distance, final boolean directed) {
+ // Can't add edges with a null source or target
+ if (source == null)
+ throw new NullPointerException("The source vertex cannot be null");
+ else if (target == null) throw new NullPointerException("The target vertex cannot be null");
+
+ // Initialize adjacency list for vertices if necessary
+ if (!backing.containsKey(source)) {
+ backing.put(source, new FunctionalMap<T, Integer>());
+ }
+
+ // Add the edge to the graph
+ backing.get(source).put(target, distance);
+
+ // Handle possible directed edges
+ if (!directed) {
+ if (!backing.containsKey(target)) {
+ backing.put(target, new FunctionalMap<T, Integer>());
+ }
+
+ backing.get(target).put(source, distance);
+ }
+ }
+
+ /**
+ * Execute an action for all edges of a specific vertex matching
+ * conditions
+ *
+ * @param source
+ * The vertex to test edges for
+ * @param matcher
+ * The conditions an edge must match
+ * @param action
+ * The action to execute for matching edges
+ */
+ public void forAllEdgesMatchingAt(final T source, final BiPredicate<T, Integer> matcher,
+ final BiConsumer<T, Integer> action) {
+ if (matcher == null)
+ throw new NullPointerException("Matcher must not be null");
+ else if (action == null) throw new NullPointerException("Action must not be null");
+
+ getEdges(source).forEach((target, weight) -> {
+ if (matcher.test(target, weight)) {
+ action.accept(target, weight);
+ }
+ });
+ }
+
+ /**
+ * Get all the edges that begin at a particular source vertex
+ *
+ * @param source
+ * The vertex to use as a source
+ * @return All of the edges with the specified vertex as a source
+ */
+ public IMap<T, Integer> getEdges(final T source) {
+ // Can't find edges for a null source
+ if (source == null)
+ throw new NullPointerException("The source cannot be null.");
+ else if (!backing.containsKey(source))
+ throw new IllegalArgumentException("Vertex " + source + " is not in graph");
+
+ return backing.get(source);
+ }
+
+ /**
+ * Get the initial vertex of the graph
+ *
+ * @return The initial vertex of the graph
+ */
+ public T getInitial() {
+ return backing.keyList().first();
+ }
+
+ /**
+ * Uses Prim's algorothm to calculate a MST for the graph.
+ *
+ * If the graph is non-connected, this will lead to unpredictable
+ * results.
+ *
+ * @return a list of edges that constitute the MST
+ */
+ public List<Edge<T>> getMinimumSpanningTree() {
+ // Set of all of the currently available edges
+ final Queue<Edge<T>> available = new PriorityQueue<>(10,
+ (left, right) -> left.getDistance() - right.getDistance());
+
+ // The MST of the graph
+ final List<Edge<T>> minimums = new ArrayList<>();
+
+ // The set of all of the visited vertices.
+ final Set<T> visited = new HashSet<>();
+
+ // Start at the initial vertex and visit it
+ final IHolder<T> source = new Identity<>(getInitial());
+
+ visited.add(source.getValue());
+
+ // Make sure we visit all the nodes
+ while (visited.size() != getVertexCount()) {
+ // Grab all edges adjacent to the provided edge
+
+ forAllEdgesMatchingAt(source.getValue(), (target, weight) -> {
+ return !visited.contains(target);
+ }, (target, weight) -> {
+ final T vert = source.unwrap(vertex -> vertex);
+
+ available.add(new Edge<>(vert, target, weight));
+ });
+
+ // Get the edge with the minimum distance
+ final IHolder<Edge<T>> minimum = new Identity<>(available.poll());
+
+ // Only consider edges where we haven't visited the
+ // target of
+ // the edge
+ while (visited.contains(minimum.getValue().getTarget())) {
+ minimum.transform((edge) -> available.poll());
+ }
+
+ // Add it to our MST
+ minimums.add(minimum.getValue());
+
+ // Advance to the next node
+ source.transform((vertex) -> minimum.unwrap(edge -> edge.getTarget()));
+
+ // Visit this node
+ visited.add(source.getValue());
+ }
+
+ return minimums;
+ }
+
+ /**
+ * Get the count of the vertices in this graph
+ *
+ * @return A count of the vertices in this graph
+ */
+ public int getVertexCount() {
+ return backing.size();
+ }
+
+ /**
+ * Get all of the vertices in this graph.
+ *
+ * @return A unmodifiable set of all the vertices in the graph.
+ */
+ public IList<T> getVertices() {
+ return backing.keyList();
+ }
+
+ /**
+ * Remove the edge starting at the source and ending at the target
+ *
+ * @param source
+ * The source vertex for the edge
+ * @param target
+ * The target vertex for the edge
+ */
+ public void removeEdge(final T source, final T target) {
+ // Can't remove things w/ null vertices
+ if (source == null)
+ throw new NullPointerException("The source vertex cannot be null");
+ else if (target == null) throw new NullPointerException("The target vertex cannot be null");
+
+ // Can't remove if one vertice doesn't exists
+ if (!backing.containsKey(source))
+ throw new NoSuchElementException("vertex " + source + " does not exist.");
+
+ if (!backing.containsKey(target))
+ throw new NoSuchElementException("vertex " + target + " does not exist.");
+
+ backing.get(source).remove(target);
+
+ // Uncomment this to turn the graph undirected
+ // graph.get(target).remove(source);
+ }
+
+ /**
+ * Convert a graph into a adjacency map/matrix
+ *
+ * @return A adjacency map representing this graph
+ */
+ public AdjacencyMap<T> toAdjacencyMap() {
+ final AdjacencyMap<T> adjacency = new AdjacencyMap<>(backing.keyList());
+
+ backing.forEach((sourceKey, sourceValue) -> {
+ sourceValue.forEach((targetKey, targetValue) -> {
+ adjacency.setWeight(sourceKey, targetKey, targetValue);
+ });
+ });
+
+ return adjacency;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java b/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java
new file mode 100644
index 0000000..7c487eb
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/ExtensionFileFilter.java
@@ -0,0 +1,56 @@
+package bjc.utils.gui;
+
+import java.io.File;
+import java.util.List;
+
+import javax.swing.filechooser.FileFilter;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A file filter based on extensions.
+ *
+ * Built for Swing.
+ *
+ * @author ben
+ *
+ */
+public class ExtensionFileFilter extends FileFilter {
+ /**
+ * The list holding all filtered extensions
+ */
+ private final IList<String> extensions;
+
+ /**
+ * Create a new filter only showing files with the specified extensions.
+ *
+ * @param exts
+ * The extensions to show in this filter.
+ */
+ public ExtensionFileFilter(final List<String> exts) {
+ extensions = new FunctionalList<>(exts);
+ }
+
+ /**
+ * Create a new filter only showing files with the specified extensions.
+ *
+ * @param exts
+ * The extensions to show in this filter.
+ */
+ public ExtensionFileFilter(final String... exts) {
+ extensions = new FunctionalList<>(exts);
+ }
+
+ @Override
+ public boolean accept(final File pathname) {
+ if (pathname == null) throw new NullPointerException("Pathname must not be null");
+
+ return extensions.anyMatch(pathname.getName()::endsWith);
+ }
+
+ @Override
+ public String getDescription() {
+ return extensions.toString();
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/gui/SimpleDialogs.java b/base/src/main/java/bjc/utils/gui/SimpleDialogs.java
new file mode 100644
index 0000000..59eb1c3
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleDialogs.java
@@ -0,0 +1,269 @@
+package bjc.utils.gui;
+
+import java.awt.Component;
+import java.awt.Frame;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import bjc.utils.gui.layout.VLayout;
+
+/**
+ * Utility class for getting simple input from the user.
+ *
+ * @author ben
+ *
+ */
+public class SimpleDialogs {
+ /**
+ * Get a bounded integer from the user.
+ *
+ * @param parent
+ * The parent component for the dialogs.
+ * @param title
+ * The title for the dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @param lowerBound
+ * The lower integer bound to accept.
+ * @param upperBound
+ * The upper integer bound to accept.
+ * @return A int within the specified bounds.
+ */
+ public static int getBoundedInt(final Component parent, final String title, final String prompt,
+ final int lowerBound, final int upperBound) {
+ return getValue(parent, title, prompt, (strang) -> {
+ try {
+ final int value = Integer.parseInt(strang);
+
+ return value < upperBound && value > lowerBound;
+ } catch (final NumberFormatException nfex) {
+ // We don't care about the specifics of the
+ // exception, just
+ // that this value isn't good
+ return false;
+ }
+ }, Integer::parseInt);
+ }
+
+ /**
+ * Asks the user to pick an option from a series of choices.
+ *
+ * @param <E>
+ * The type of choices for the user to pick
+ *
+ * @param parent
+ * The parent frame for this dialog
+ * @param title
+ * The title of this dialog
+ * @param question
+ * The question being asked
+ * @param choices
+ * The available choices for the question
+ * @return The choice the user picked, or null if they didn't pick one
+ */
+ @SuppressWarnings("unchecked")
+ public static <E> E getChoice(final Frame parent, final String title, final String question,
+ final E... choices) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (question == null) throw new NullPointerException("Question must not be null");
+
+ final JDialog chooser = new JDialog(parent, title, true);
+ chooser.setLayout(new VLayout(2));
+
+ final JPanel questionPane = new JPanel();
+
+ final JLabel questionText = new JLabel(question);
+ final JComboBox<E> questionChoices = new JComboBox<>(choices);
+
+ questionPane.add(questionText);
+ questionPane.add(questionChoices);
+
+ final JPanel buttonPane = new JPanel();
+
+ final JButton okButton = new JButton("Ok");
+ final JButton cancelButton = new JButton("Cancel");
+
+ okButton.addActionListener((event) -> chooser.dispose());
+ cancelButton.addActionListener((event) -> chooser.dispose());
+
+ buttonPane.add(cancelButton);
+ buttonPane.add(okButton);
+
+ chooser.add(questionPane);
+ chooser.add(buttonPane);
+
+ chooser.pack();
+ chooser.setVisible(true);
+
+ return (E) questionChoices.getSelectedItem();
+ }
+
+ /**
+ * Get a integer from the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A int.
+ */
+ public static int getInt(final Component parent, final String title, final String prompt) {
+ return getValue(parent, title, prompt, strang -> {
+ try {
+ Integer.parseInt(strang);
+ return true;
+ } catch (final NumberFormatException nfex) {
+ // We don't care about this exception, just mark
+ // the value
+ // as not good
+ return false;
+ }
+ }, Integer::parseInt);
+ }
+
+ /**
+ * Get a string from the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for the dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A string.
+ */
+ public static String getString(final Component parent, final String title, final String prompt) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (prompt == null) throw new NullPointerException("Prompt must not be null");
+
+ return JOptionPane.showInputDialog(parent, prompt, title, JOptionPane.QUESTION_MESSAGE);
+ }
+
+ /**
+ * Get a value parsable from a string from the user.
+ *
+ * @param <E>
+ * The type of the value parsed from the string
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @param validator
+ * A predicate to determine if a input is valid.
+ * @param transformer
+ * The function to transform the string into a value.
+ * @return The value parsed from a string.
+ */
+ public static <E> E getValue(final Component parent, final String title, final String prompt,
+ final Predicate<String> validator, final Function<String, E> transformer) {
+ if (validator == null)
+ throw new NullPointerException("Validator must not be null");
+ else if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ String input = getString(parent, title, prompt);
+
+ while (!validator.test(input)) {
+ showError(parent, "I/O Error", "Please enter a valid value");
+
+ input = getString(parent, title, prompt);
+ }
+
+ return transformer.apply(input);
+ }
+
+ /**
+ * Get a whole number from the user.
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A whole number.
+ */
+ public static int getWhole(final Component parent, final String title, final String prompt) {
+ return getBoundedInt(parent, title, prompt, 0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Ask the user a Yes/No question.
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param question
+ * The question to ask the user.
+ * @return True if the user said yes, false otherwise.
+ */
+ public static boolean getYesNo(final Component parent, final String title, final String question) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (question == null) throw new NullPointerException("Question must not be null");
+
+ final int result = JOptionPane.showConfirmDialog(parent, question, title, JOptionPane.YES_NO_OPTION);
+
+ return result == JOptionPane.YES_OPTION ? true : false;
+ }
+
+ /**
+ * Show a error message to the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param message
+ * The error to show the user.
+ */
+ public static void showError(final Component parent, final String title, final String message) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (message == null) throw new NullPointerException("Error message must not be null");
+
+ JOptionPane.showMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE);
+ }
+
+ /**
+ * Show an informative message to the user
+ *
+ * @param parent
+ * The parent for this dialog
+ * @param title
+ * Show the title for this dialog
+ * @param message
+ * Show the message for this dialog
+ */
+ public static void showMessage(final Component parent, final String title, final String message) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (message == null) throw new NullPointerException("Message must not be null");
+
+ JOptionPane.showMessageDialog(parent, title, message, JOptionPane.INFORMATION_MESSAGE);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java b/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java
new file mode 100644
index 0000000..7da0bd8
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleFileChooser.java
@@ -0,0 +1,198 @@
+package bjc.utils.gui;
+
+import java.awt.Component;
+import java.io.File;
+
+import javax.swing.JFileChooser;
+
+import bjc.utils.exceptions.FileNotChosenException;
+
+/**
+ * Utility class for easily prompting user for files.
+ *
+ * Built for Swing.
+ *
+ * @author ben
+ *
+ */
+public class SimpleFileChooser {
+ private static File doOpenFile(final Component parent, final String title, final JFileChooser files) {
+ if (title == null) throw new NullPointerException("Title must not be null");
+
+ files.setDialogTitle(title);
+
+ boolean success = false;
+
+ while (!success) {
+ try {
+ maybeDoOpenFile(parent, files);
+
+ success = true;
+ } catch (final FileNotChosenException fncx) {
+ // We don't care about specifics
+ SimpleDialogs.showError(parent, "I/O Error", "Please pick a file to open");
+ }
+ }
+
+ return files.getSelectedFile();
+ }
+
+ private static File doSaveFile(final Component parent, final String title, final JFileChooser files) {
+ if (title == null) throw new NullPointerException("Title must not be null");
+
+ files.setDialogTitle(title);
+
+ final boolean success = false;
+
+ while (!success) {
+ try {
+ maybeDoSaveFile(parent, files);
+
+ return files.getSelectedFile();
+ } catch (final FileNotChosenException fncex) {
+ // We don't care about specifics
+ SimpleDialogs.showError(parent, "I/O Error", "Please pick a file to save to");
+ }
+ }
+ }
+
+ /**
+ * Prompt the user with a "Open File..." dialog. Keeps prompting them
+ * until they pick a file.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @return The file the user has chosen.
+ */
+ public static File getOpenFile(final Component parent, final String title) {
+ final JFileChooser files = new JFileChooser();
+
+ return doOpenFile(parent, title, files);
+ }
+
+ /**
+ * Prompt the user with a "Open File..." dialog. Keeps prompting them
+ * until they pick a file.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @param extensions
+ * The list of file extensions the file should have.
+ * @return The file the user has chosen.
+ */
+ public static File getOpenFile(final Component parent, final String title, final String... extensions) {
+ final JFileChooser files = new JFileChooser();
+
+ files.addChoosableFileFilter(new ExtensionFileFilter(extensions));
+
+ return doOpenFile(parent, title, files);
+ }
+
+ /**
+ * Prompt the user with a "Save File..." dialog.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @return The file the user chose.
+ */
+ public static File getSaveFile(final Component parent, final String title) {
+ final JFileChooser files = new JFileChooser();
+
+ return doSaveFile(parent, title, files);
+ }
+
+ /**
+ * Prompt the user with a "Save File..." dialog.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @param extensions
+ * The extensions of the files the user can choose.
+ * @return The file the user chose.
+ */
+ public static File getSaveFile(final Component parent, final String title, final String... extensions) {
+ final JFileChooser files = new JFileChooser();
+
+ files.addChoosableFileFilter(new ExtensionFileFilter(extensions));
+
+ return doSaveFile(parent, title, files);
+ }
+
+ private static void maybeDoOpenFile(final Component parent, final JFileChooser files)
+ throws FileNotChosenException {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (files == null) throw new NullPointerException("File chooser must not be null");
+
+ final int result = files.showSaveDialog(parent);
+
+ if (result != JFileChooser.APPROVE_OPTION) throw new FileNotChosenException();
+ }
+
+ private static void maybeDoSaveFile(final Component parent, final JFileChooser files)
+ throws FileNotChosenException {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (files == null) throw new NullPointerException("File chooser must not be null");
+
+ final int result = files.showSaveDialog(parent);
+
+ if (result != JFileChooser.APPROVE_OPTION) throw new FileNotChosenException();
+ }
+
+ /**
+ * Prompt the user with a "Open File..." dialog.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @return The file if the user chose one or null if they didn't.
+ */
+ public static File maybeOpenFile(final Component parent, final String title) {
+ if (title == null) throw new NullPointerException("Title must not be null");
+
+ final JFileChooser files = new JFileChooser();
+ files.setDialogTitle(title);
+
+ try {
+ maybeDoOpenFile(parent, files);
+ } catch (final FileNotChosenException fncex) {
+ // We don't care about specifics
+ }
+
+ return files.getSelectedFile();
+ }
+
+ /**
+ * Prompt the user with a "Save File..." dialog.
+ *
+ * @param parent
+ * The component to use as the parent for the dialog.
+ * @param title
+ * The title of the dialog to prompt with.
+ * @return The file if the user chose one or null if they didn't.
+ */
+ public static File maybeSaveFile(final Component parent, final String title) {
+ if (title == null) throw new NullPointerException("Title must not be null");
+
+ final JFileChooser files = new JFileChooser();
+ files.setDialogTitle(title);
+
+ try {
+ maybeDoSaveFile(parent, files);
+ } catch (final FileNotChosenException fncex) {
+ // We don't care about specifics
+ }
+
+ return files.getSelectedFile();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java b/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java
new file mode 100644
index 0000000..5237557
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleInternalDialogs.java
@@ -0,0 +1,208 @@
+package bjc.utils.gui;
+
+import java.awt.Component;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.swing.JOptionPane;
+
+/**
+ * Utility class for getting simple input from the user.
+ *
+ * Modified to work with JDesktopPanes
+ *
+ * @author ben
+ *
+ */
+public class SimpleInternalDialogs {
+ /**
+ * Get a bounded integer from the user.
+ *
+ * @param parent
+ * The parent component for the dialogs.
+ * @param title
+ * The title for the dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @param lowerBound
+ * The lower integer bound to accept.
+ * @param upperBound
+ * The upper integer bound to accept.
+ * @return A int within the specified bounds.
+ */
+ public static int getBoundedInt(final Component parent, final String title, final String prompt,
+ final int lowerBound, final int upperBound) {
+ return getValue(parent, title, prompt, (strang) -> {
+ try {
+ final int value = Integer.parseInt(strang);
+
+ return value < upperBound && value > lowerBound;
+ } catch (final NumberFormatException nfex) {
+ // We don't care about the specifics of the
+ // exception, just
+ // that this value isn't good
+ return false;
+ }
+ }, Integer::parseInt);
+ }
+
+ /**
+ * Get a integer from the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A int.
+ */
+ public static int getInt(final Component parent, final String title, final String prompt) {
+ return getValue(parent, title, prompt, strang -> {
+ try {
+ Integer.parseInt(strang);
+ return true;
+ } catch (final NumberFormatException nfex) {
+ // We don't care about this exception, just mark
+ // the value
+ // as not good
+ return false;
+ }
+ }, Integer::parseInt);
+ }
+
+ /**
+ * Get a string from the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for the dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A string.
+ */
+ public static String getString(final Component parent, final String title, final String prompt) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (prompt == null) throw new NullPointerException("Prompt must not be null");
+
+ return JOptionPane.showInternalInputDialog(parent, prompt, title, JOptionPane.QUESTION_MESSAGE);
+ }
+
+ /**
+ * Get a value parsable from a string from the user.
+ *
+ * @param <E>
+ * The type of the value parsed from the string
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @param validator
+ * A predicate to determine if a input is valid.
+ * @param transformer
+ * The function to transform the string into a value.
+ * @return The value parsed from a string.
+ */
+ public static <E> E getValue(final Component parent, final String title, final String prompt,
+ final Predicate<String> validator, final Function<String, E> transformer) {
+ if (validator == null)
+ throw new NullPointerException("Validator must not be null");
+ else if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ String strang = getString(parent, title, prompt);
+
+ while (!validator.test(strang)) {
+ showError(parent, "I/O Error", "Please enter a valid value");
+
+ strang = getString(parent, title, prompt);
+ }
+
+ return transformer.apply(strang);
+ }
+
+ /**
+ * Get a whole number from the user.
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param prompt
+ * The prompt to tell the user what to enter.
+ * @return A whole number.
+ */
+ public static int getWhole(final Component parent, final String title, final String prompt) {
+ return getBoundedInt(parent, title, prompt, 0, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Ask the user a Yes/No question.
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param question
+ * The question to ask the user.
+ * @return True if the user said yes, false otherwise.
+ */
+ public static boolean getYesNo(final Component parent, final String title, final String question) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (question == null) throw new NullPointerException("Question must not be null");
+
+ final int result = JOptionPane.showInternalConfirmDialog(parent, question, title,
+ JOptionPane.YES_NO_OPTION);
+
+ return result == JOptionPane.YES_OPTION ? true : false;
+ }
+
+ /**
+ * Show a error message to the user
+ *
+ * @param parent
+ * The parent component for dialogs.
+ * @param title
+ * The title for dialogs.
+ * @param message
+ * The error to show the user.
+ */
+ public static void showError(final Component parent, final String title, final String message) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (message == null) throw new NullPointerException("Error message must not be null");
+
+ JOptionPane.showInternalMessageDialog(parent, message, title, JOptionPane.ERROR_MESSAGE);
+ }
+
+ /**
+ * Show an informative message to the user
+ *
+ * @param parent
+ * The parent for this dialog
+ * @param title
+ * Show the title for this dialog
+ * @param message
+ * Show the message for this dialog
+ */
+ public static void showMessage(final Component parent, final String title, final String message) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null)
+ throw new NullPointerException("Title must not be null");
+ else if (message == null) throw new NullPointerException("Message must not be null");
+
+ JOptionPane.showInternalMessageDialog(parent, title, message, JOptionPane.INFORMATION_MESSAGE);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java b/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java
new file mode 100644
index 0000000..afb498e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleInternalFrame.java
@@ -0,0 +1,40 @@
+package bjc.utils.gui;
+
+import javax.swing.JInternalFrame;
+
+/**
+ * A simple internal frame class
+ *
+ * @author ben
+ *
+ */
+public class SimpleInternalFrame extends JInternalFrame {
+ private static final long serialVersionUID = -2966801321260716617L;
+
+ /**
+ * Create a new blank internal frame
+ */
+ public SimpleInternalFrame() {
+ super();
+ }
+
+ /**
+ * Create a new blank internal frame with a specific title
+ *
+ * @param title
+ * The title of the internal frame
+ */
+ public SimpleInternalFrame(final String title) {
+ super(title);
+ }
+
+ protected void setupFrame() {
+ setSize(320, 240);
+
+ setResizable(true);
+
+ setClosable(true);
+ setMaximizable(true);
+ setIconifiable(true);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/gui/SimpleJList.java b/base/src/main/java/bjc/utils/gui/SimpleJList.java
new file mode 100644
index 0000000..411d0db
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleJList.java
@@ -0,0 +1,49 @@
+package bjc.utils.gui;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JList;
+import javax.swing.ListModel;
+
+/**
+ * Utility class for making JLists and their models.
+ *
+ * @author ben
+ *
+ */
+public class SimpleJList {
+ /**
+ * Create a new JList from a given list.
+ *
+ * @param <E>
+ * The type of data in the JList
+ *
+ * @param source
+ * The list to populate the JList with.
+ * @return A JList populated with the elements from ls.
+ */
+ public static <E> JList<E> buildFromList(final Iterable<E> source) {
+ if (source == null) throw new NullPointerException("Source must not be null");
+
+ return new JList<>(buildModel(source));
+ }
+
+ /**
+ * Create a new list model from a given list.
+ *
+ * @param <E>
+ * The type of data in the list model
+ *
+ * @param source
+ * The list to fill the list model from.
+ * @return A list model populated with the elements from ls.
+ */
+ public static <E> ListModel<E> buildModel(final Iterable<E> source) {
+ if (source == null) throw new NullPointerException("Source must not be null");
+
+ final DefaultListModel<E> defaultModel = new DefaultListModel<>();
+
+ source.forEach(defaultModel::addElement);
+
+ return defaultModel;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java b/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java
new file mode 100644
index 0000000..9b01507
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/SimpleTitledBorder.java
@@ -0,0 +1,25 @@
+package bjc.utils.gui;
+
+import javax.swing.border.EtchedBorder;
+import javax.swing.border.TitledBorder;
+
+/**
+ * A simple border with a title attached to it.
+ *
+ * @author ben
+ *
+ */
+public class SimpleTitledBorder extends TitledBorder {
+ // Version ID for serialization
+ private static final long serialVersionUID = -5655969079949148487L;
+
+ /**
+ * Create a new border with the specified title.
+ *
+ * @param title
+ * The title for the border.
+ */
+ public SimpleTitledBorder(final String title) {
+ super(new EtchedBorder(), title);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java b/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java
new file mode 100644
index 0000000..fbc58ed
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/TextAreaOutputStream.java
@@ -0,0 +1,35 @@
+package bjc.utils.gui;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.swing.JTextArea;
+
+/**
+ * An output stream that prints to a JTextArea
+ *
+ * @author epr
+ * @author Levente S\u00e1ntha (lsantha@users.sourceforge.net)
+ */
+public class TextAreaOutputStream extends OutputStream {
+ private final JTextArea textArea;
+
+ /**
+ * Create a new output stream attached to a textarea
+ *
+ * @param console
+ * The textarea to write to
+ */
+ public TextAreaOutputStream(final JTextArea console) {
+ this.textArea = console;
+ }
+
+ @Override
+ public void write(final int b) throws IOException {
+ textArea.append("" + (char) b);
+
+ if (b == '\n') {
+ textArea.repaint();
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java b/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java
new file mode 100644
index 0000000..eb60ae2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/awt/ExtensionFileFilter.java
@@ -0,0 +1,50 @@
+package bjc.utils.gui.awt;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.List;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Filter a set of filenames by extension.
+ *
+ * Built for AWT
+ *
+ * @author ben
+ *
+ */
+public class ExtensionFileFilter implements FilenameFilter {
+ /**
+ * The list of extensions to filter
+ */
+ private final IList<String> extensions;
+
+ /**
+ * Create a new filter only showing files with the specified extensions.
+ *
+ * @param exts
+ * The extensions to show in this filter.
+ */
+ public ExtensionFileFilter(final List<String> exts) {
+ if (exts == null) throw new NullPointerException("Extensions must not be null");
+
+ extensions = new FunctionalList<>(exts);
+ }
+
+ /**
+ * Create a new filter only showing files with the specified extensions.
+ *
+ * @param exts
+ * The extensions to show in this filter.
+ */
+ public ExtensionFileFilter(final String... exts) {
+ extensions = new FunctionalList<>(exts);
+ }
+
+ @Override
+ public boolean accept(final File directory, final String name) {
+ return extensions.anyMatch(name::endsWith);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java b/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java
new file mode 100644
index 0000000..77a4a59
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/awt/SimpleFileDialog.java
@@ -0,0 +1,144 @@
+package bjc.utils.gui.awt;
+
+import java.awt.FileDialog;
+import java.awt.Frame;
+import java.io.File;
+import java.io.FilenameFilter;
+
+import bjc.utils.gui.SimpleDialogs;
+
+/**
+ * A simple way to get the user to pick a file
+ *
+ * Built for AWT.
+ *
+ * @author ben
+ *
+ */
+public class SimpleFileDialog {
+ /**
+ * Prompt the user to pick a file to open
+ *
+ * @param parent
+ * The parent of the file picker
+ * @param title
+ * The title of the file picker
+ * @return The file the user picked
+ */
+ public static File getOpenFile(final Frame parent, final String title) {
+ return getOpenFile(parent, title, (String[]) null);
+ }
+
+ /**
+ * Prompt the user to pick a file to open
+ *
+ * @param parent
+ * The parent of the file picker
+ * @param title
+ * The title of the file picker
+ * @param extensions
+ * The extensions to accept as valid
+ * @return The file the user picked
+ */
+ public static File getOpenFile(final Frame parent, final String title, final String... extensions) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null) throw new NullPointerException("Title must not be null");
+
+ final FileDialog chooser = new FileDialog(parent, title, FileDialog.LOAD);
+
+ if (extensions != null) {
+ final FilenameFilter filter = new ExtensionFileFilter(extensions);
+ chooser.setFilenameFilter(filter);
+ }
+
+ chooser.setVisible(true);
+
+ while (chooser.getFile() == null) {
+ SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to open.");
+ chooser.setVisible(true);
+ }
+
+ return chooser.getFiles()[0];
+ }
+
+ /**
+ * Prompt the user to pick a file to open
+ *
+ * @param parent
+ * The parent of the file picker
+ * @param title
+ * The title of the file picker
+ * @param extensions
+ * The extensions to accept as valid
+ * @return The file the user picked
+ */
+ public static File[] getOpenFiles(final Frame parent, final String title, final String... extensions) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null) throw new NullPointerException("Title must not be null");
+
+ final FileDialog chooser = new FileDialog(parent, title, FileDialog.LOAD);
+
+ if (extensions != null) {
+ final FilenameFilter filter = new ExtensionFileFilter(extensions);
+ chooser.setFilenameFilter(filter);
+ }
+
+ chooser.setMultipleMode(true);
+ chooser.setVisible(true);
+
+ while (chooser.getFile() == null) {
+ SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to open.");
+ chooser.setVisible(true);
+ }
+
+ return chooser.getFiles();
+ }
+
+ /**
+ * Prompt the user to pick a file to save
+ *
+ * @param parent
+ * The parent of the file picker
+ * @param title
+ * The title of the file picker
+ * @return The file the user picked
+ */
+ public static File getSaveFile(final Frame parent, final String title) {
+ return getSaveFile(parent, title, (String[]) null);
+ }
+
+ /**
+ * Prompt the user to pick a file to save
+ *
+ * @param parent
+ * The parent of the file picker
+ * @param title
+ * The title of the file picker
+ * @param extensions
+ * The extensions to accept as valid
+ * @return The file the user picked
+ */
+ public static File getSaveFile(final Frame parent, final String title, final String... extensions) {
+ if (parent == null)
+ throw new NullPointerException("Parent must not be null");
+ else if (title == null) throw new NullPointerException("Title must not be null");
+
+ final FileDialog chooser = new FileDialog(parent, title, FileDialog.SAVE);
+
+ if (extensions != null) {
+ final FilenameFilter filter = new ExtensionFileFilter(extensions);
+ chooser.setFilenameFilter(filter);
+ }
+
+ chooser.setVisible(true);
+
+ while (chooser.getFile() == null) {
+ SimpleDialogs.showError(parent, "File I/O Error", "Please choose a file to save to.");
+ chooser.setVisible(true);
+ }
+
+ return chooser.getFiles()[0];
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java b/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java
new file mode 100644
index 0000000..6f384f2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/layout/AutosizeLayout.java
@@ -0,0 +1,22 @@
+package bjc.utils.gui.layout;
+
+import java.awt.GridLayout;
+
+/**
+ * A layout that simply holds one component that it auto-resizes whenever it is
+ * resized.
+ *
+ * @author ben
+ *
+ */
+public class AutosizeLayout extends GridLayout {
+ // Version id for serialization
+ private static final long serialVersionUID = -2495693595953396924L;
+
+ /**
+ * Create a new auto-size layout.
+ */
+ public AutosizeLayout() {
+ super(1, 1);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/layout/HLayout.java b/base/src/main/java/bjc/utils/gui/layout/HLayout.java
new file mode 100644
index 0000000..4ed1661
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/layout/HLayout.java
@@ -0,0 +1,25 @@
+package bjc.utils.gui.layout;
+
+import java.awt.GridLayout;
+
+/**
+ * A layout manager that lays out its components horizontally, evenly sizing
+ * them.
+ *
+ * @author ben
+ *
+ */
+public class HLayout extends GridLayout {
+ // Version ID for serialization
+ private static final long serialVersionUID = 1244964456966270026L;
+
+ /**
+ * Create a new horizontal layout with the specified number of columns.
+ *
+ * @param columns
+ * The number of columns in this layout.
+ */
+ public HLayout(final int columns) {
+ super(1, columns);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/layout/VLayout.java b/base/src/main/java/bjc/utils/gui/layout/VLayout.java
new file mode 100644
index 0000000..6993365
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/layout/VLayout.java
@@ -0,0 +1,25 @@
+package bjc.utils.gui.layout;
+
+import java.awt.GridLayout;
+
+/**
+ * A layout that lays out its components vertically, evenly sharing space among
+ * them.
+ *
+ * @author ben
+ *
+ */
+public class VLayout extends GridLayout {
+ // Version ID for serializations
+ private static final long serialVersionUID = -6417962941602322663L;
+
+ /**
+ * Create a new vertical layout with the specified number of rows.
+ *
+ * @param rows
+ * The number of rows.
+ */
+ public VLayout(final int rows) {
+ super(rows, 1);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java b/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java
new file mode 100644
index 0000000..4f71d38
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/DropdownListPanel.java
@@ -0,0 +1,73 @@
+package bjc.utils.gui.panels;
+
+import java.awt.BorderLayout;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.ListSelectionModel;
+
+import bjc.utils.funcdata.IList;
+import bjc.utils.gui.layout.AutosizeLayout;
+import bjc.utils.gui.layout.HLayout;
+
+/**
+ * A panel that allows you to select choices from a dropdown list
+ *
+ * @author ben
+ *
+ */
+public class DropdownListPanel extends JPanel {
+ private static final long serialVersionUID = 2719963952350133541L;
+
+ /**
+ * Create a new dropdown list panel
+ *
+ * @param <T>
+ * The type of items in the dropdown list
+ * @param type
+ * The label of the type of items in the list
+ * @param model
+ * The model to put items into
+ * @param choices
+ * The items to choose from
+ */
+ public <T> DropdownListPanel(final String type, final DefaultListModel<T> model, final IList<T> choices) {
+ setLayout(new AutosizeLayout());
+
+ final JPanel itemInputPanel = new JPanel();
+ itemInputPanel.setLayout(new BorderLayout());
+
+ final JPanel addItemPanel = new JPanel();
+ addItemPanel.setLayout(new HLayout(2));
+
+ final JComboBox<T> addItemBox = new JComboBox<>();
+ choices.forEach(addItemBox::addItem);
+
+ final JButton addItemButton = new JButton("Add " + type);
+
+ addItemPanel.add(addItemBox);
+ addItemPanel.add(addItemButton);
+
+ final JList<T> itemList = new JList<>(model);
+ itemList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ final JButton removeItemButton = new JButton("Remove " + type);
+
+ addItemButton.addActionListener((ev) -> {
+ model.addElement(addItemBox.getItemAt(addItemBox.getSelectedIndex()));
+ });
+
+ removeItemButton.addActionListener((ev) -> {
+ model.remove(itemList.getSelectedIndex());
+ });
+
+ itemInputPanel.add(addItemPanel, BorderLayout.PAGE_START);
+ itemInputPanel.add(itemList, BorderLayout.CENTER);
+ itemInputPanel.add(removeItemButton, BorderLayout.PAGE_END);
+
+ add(itemInputPanel);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java
new file mode 100644
index 0000000..2cecf0c
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/FormattedInputPanel.java
@@ -0,0 +1,66 @@
+package bjc.utils.gui.panels;
+
+import java.util.function.Consumer;
+
+import javax.swing.JFormattedTextField;
+import javax.swing.JFormattedTextField.AbstractFormatter;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import bjc.utils.gui.layout.HLayout;
+
+/**
+ * A simple panel allowing for input of a single formatted value
+ *
+ * @author ben
+ *
+ * @param <InputVal>
+ * The type of value being formatted
+ */
+public class FormattedInputPanel<InputVal> extends JPanel {
+ private static final long serialVersionUID = 5232016563558588031L;
+
+ private final JFormattedTextField field;
+
+ /**
+ * Create a new formatted input panel
+ *
+ * @param label
+ * The label for this panel
+ * @param length
+ * The length of this panel
+ * @param formatter
+ * The formatter to use for input
+ * @param reciever
+ * The action to call whenever the value changes
+ */
+ @SuppressWarnings("unchecked")
+ public FormattedInputPanel(final String label, final int length, final AbstractFormatter formatter,
+ final Consumer<InputVal> reciever) {
+ setLayout(new HLayout(2));
+
+ final JLabel lab = new JLabel(label);
+ field = new JFormattedTextField(formatter);
+
+ field.setColumns(length);
+ field.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
+ field.addPropertyChangeListener("value", (event) -> {
+ // This is safe, because InputVal should be the type of
+ // whatever object the formatter is returning
+ reciever.accept((InputVal) field.getValue());
+ });
+
+ add(lab);
+ add(field);
+ }
+
+ /**
+ * Reset the value in this panel to a specified value
+ *
+ * @param value
+ * The value to set the panel to
+ */
+ public void resetValues(final InputVal value) {
+ field.setValue(value);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java b/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java
new file mode 100644
index 0000000..653dace
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/HolderOutputPanel.java
@@ -0,0 +1,79 @@
+package bjc.utils.gui.panels;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.Timer;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.gui.layout.HLayout;
+
+/**
+ * A panel that outputs a value bound to a {@link IHolder}
+ *
+ * @author ben
+ *
+ */
+public class HolderOutputPanel extends JPanel {
+ private static final long serialVersionUID = 166573313903782080L;
+
+ private Timer updater;
+ private final JLabel value;
+ private final int nDelay;
+ private final IHolder<String> val;
+
+ /**
+ * Create a new display panel, backed by a holder
+ *
+ * @param lab
+ * The label to attach to this field
+ * @param valueHolder
+ * The holder to get the value from
+ * @param nDelay
+ * The delay in ms between value updates
+ */
+ public HolderOutputPanel(final String lab, final IHolder<String> valueHolder, final int nDelay) {
+ this.val = valueHolder;
+ this.nDelay = nDelay;
+
+ setLayout(new HLayout(2));
+
+ final JLabel label = new JLabel(lab);
+ value = new JLabel("(stopped)");
+
+ updater = new Timer(nDelay, (event) -> {
+ value.setText(valueHolder.getValue());
+ });
+
+ add(label);
+ add(value);
+ }
+
+ /**
+ * Set this panel back to its initial state
+ */
+ public void reset() {
+ stopUpdating();
+
+ value.setText("(stopped)");
+
+ updater = new Timer(nDelay, (event) -> {
+ value.setText(val.getValue());
+ });
+ }
+
+ /**
+ * Start updating the contents of the field from the holder
+ */
+ public void startUpdating() {
+ updater.start();
+ }
+
+ /**
+ * Stop updating the contents of the field from the holder
+ */
+ public void stopUpdating() {
+ updater.stop();
+
+ value.setText(value.getText() + " (stopped)");
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java b/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java
new file mode 100644
index 0000000..cca73d5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/ListParameterPanel.java
@@ -0,0 +1,133 @@
+package bjc.utils.gui.panels;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.ListSelectionModel;
+
+import bjc.utils.funcdata.IList;
+import bjc.utils.gui.SimpleJList;
+import bjc.utils.gui.layout.HLayout;
+import bjc.utils.gui.layout.VLayout;
+
+/**
+ * A panel that has a list of objects and ways of manipulating that list
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of data stored in the list
+ */
+public class ListParameterPanel<E> extends JPanel {
+ // Version id for serialization
+ private static final long serialVersionUID = 3442971104975491571L;
+
+ /**
+ * Create a new panel using the specified actions for doing things
+ *
+ * @param add
+ * The action that provides items
+ * @param edit
+ * The action that edits items
+ * @param remove
+ * The action that removes items
+ */
+ public ListParameterPanel(final Supplier<E> add, final Consumer<E> edit, final Consumer<E> remove) {
+ this(add, edit, remove, null);
+ }
+
+ /**
+ * Create a new panel using the specified actions for doing things
+ *
+ * @param add
+ * The action that provides items
+ * @param edit
+ * The action that edits items
+ * @param remove
+ * The action that removes items
+ * @param defaults
+ * The default values to put in the list
+ */
+ public ListParameterPanel(final Supplier<E> add, final Consumer<E> edit, final Consumer<E> remove,
+ final IList<E> defaults) {
+ setLayout(new VLayout(2));
+
+ JList<E> list;
+
+ if (defaults != null) {
+ list = SimpleJList.buildFromList(defaults.toIterable());
+ } else {
+ list = new JList<>(new DefaultListModel<>());
+ }
+
+ list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ final JPanel buttonPanel = new JPanel();
+
+ int numButtons = 0;
+
+ if (add != null) {
+ numButtons++;
+ }
+
+ if (edit != null) {
+ numButtons++;
+ }
+
+ if (remove != null) {
+ numButtons++;
+ }
+
+ buttonPanel.setLayout(new HLayout(numButtons));
+
+ JButton addParam = null;
+
+ if (add != null) {
+ addParam = new JButton("Add...");
+ addParam.addActionListener((event) -> {
+ final DefaultListModel<E> model = (DefaultListModel<E>) list.getModel();
+
+ model.addElement(add.get());
+ });
+ }
+
+ JButton editParam = null;
+
+ if (edit != null) {
+ editParam = new JButton("Edit...");
+ editParam.addActionListener((event) -> {
+ edit.accept(list.getSelectedValue());
+ });
+ }
+
+ JButton removeParam = null;
+
+ if (remove != null) {
+ removeParam = new JButton("Remove...");
+ removeParam.addActionListener((event) -> {
+ final DefaultListModel<E> model = (DefaultListModel<E>) list.getModel();
+
+ remove.accept(model.remove(list.getSelectedIndex()));
+ });
+ }
+
+ if (add != null) {
+ buttonPanel.add(addParam);
+ }
+
+ if (edit != null) {
+ buttonPanel.add(editParam);
+ }
+
+ if (remove != null) {
+ buttonPanel.add(removeParam);
+ }
+
+ add(list);
+ add(buttonPanel);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java
new file mode 100644
index 0000000..65c533d
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/SimpleInputPanel.java
@@ -0,0 +1,45 @@
+package bjc.utils.gui.panels;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+/**
+ * A simple component for text input
+ *
+ * @author ben
+ *
+ */
+public class SimpleInputPanel extends JPanel {
+ private static final long serialVersionUID = -4734279623645236868L;
+
+ /**
+ * The text field containing the input value
+ */
+ public final JTextField inputValue;
+
+ /**
+ * Create a new input panel
+ *
+ * @param label
+ * The label for the field
+ * @param columns
+ * The number of columns of text input to take
+ */
+ public SimpleInputPanel(final String label, final int columns) {
+ setLayout(new BorderLayout());
+
+ final JLabel inputLabel = new JLabel(label);
+
+ if (columns < 1) {
+ inputValue = new JTextField();
+ } else {
+ inputValue = new JTextField(columns);
+ }
+
+ add(inputLabel, BorderLayout.LINE_START);
+ add(inputValue, BorderLayout.CENTER);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java
new file mode 100644
index 0000000..edc1797
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/SimpleListPanel.java
@@ -0,0 +1,93 @@
+package bjc.utils.gui.panels;
+
+import java.awt.BorderLayout;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+
+import bjc.utils.gui.layout.AutosizeLayout;
+import bjc.utils.gui.layout.HLayout;
+
+/**
+ * A simple list of strings
+ *
+ * @author ben
+ *
+ */
+public class SimpleListPanel extends JPanel {
+ private static final long serialVersionUID = 2719963952350133541L;
+
+ private static void addItem(final DefaultListModel<String> model, final Predicate<String> verifier,
+ final Consumer<String> onFailure, final JTextField addItemField) {
+ final String potentialItem = addItemField.getText();
+
+ if (verifier == null || verifier.test(potentialItem)) {
+ model.addElement(potentialItem);
+ } else {
+ onFailure.accept(potentialItem);
+ }
+
+ addItemField.setText("");
+ }
+
+ /**
+ * Create a new list panel
+ *
+ * @param type
+ * The type of things in the list
+ * @param model
+ * The model to put items into
+ * @param verifier
+ * The predicate to use to verify items
+ * @param onFailure
+ * The function to call when an item doesn't verify
+ */
+ public SimpleListPanel(final String type, final DefaultListModel<String> model,
+ final Predicate<String> verifier, final Consumer<String> onFailure) {
+ setLayout(new AutosizeLayout());
+
+ final JPanel itemInputPanel = new JPanel();
+ itemInputPanel.setLayout(new BorderLayout());
+
+ final JPanel addItemPanel = new JPanel();
+ addItemPanel.setLayout(new HLayout(2));
+
+ final JTextField addItemField = new JTextField(255);
+ final JButton addItemButton = new JButton("Add " + type);
+
+ addItemPanel.add(addItemField);
+ addItemPanel.add(addItemButton);
+
+ final JList<String> itemList = new JList<>(model);
+ itemList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ final JScrollPane listScroller = new JScrollPane(itemList);
+
+ final JButton removeItemButton = new JButton("Remove " + type);
+
+ addItemButton.addActionListener((ev) -> {
+ addItem(model, verifier, onFailure, addItemField);
+ });
+
+ addItemField.addActionListener((ev) -> {
+ addItem(model, verifier, onFailure, addItemField);
+ });
+
+ removeItemButton.addActionListener((ev) -> {
+ model.remove(itemList.getSelectedIndex());
+ });
+
+ itemInputPanel.add(addItemPanel, BorderLayout.PAGE_START);
+ itemInputPanel.add(listScroller, BorderLayout.CENTER);
+ itemInputPanel.add(removeItemButton, BorderLayout.PAGE_END);
+
+ add(itemInputPanel);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java b/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java
new file mode 100644
index 0000000..6106182
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/SimpleSpinnerPanel.java
@@ -0,0 +1,42 @@
+package bjc.utils.gui.panels;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerModel;
+
+/**
+ * A simple spinner control
+ *
+ * @author ben
+ *
+ */
+public class SimpleSpinnerPanel extends JPanel {
+ private static final long serialVersionUID = -4734279623645236868L;
+
+ /**
+ * The spinner being used
+ */
+ public final JSpinner inputValue;
+
+ /**
+ * Create a new spinner panel
+ *
+ * @param label
+ * The label for the spinner
+ * @param model
+ * The model to attach to the spinner
+ */
+ public SimpleSpinnerPanel(final String label, final SpinnerModel model) {
+ setLayout(new BorderLayout());
+
+ final JLabel inputLabel = new JLabel(label);
+
+ inputValue = new JSpinner(model);
+
+ add(inputLabel, BorderLayout.LINE_START);
+ add(inputValue, BorderLayout.CENTER);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java b/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java
new file mode 100644
index 0000000..e6a6da4
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/SliderInputPanel.java
@@ -0,0 +1,187 @@
+package bjc.utils.gui.panels;
+
+import java.text.ParseException;
+import java.util.function.Consumer;
+
+import javax.swing.JFormattedTextField;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+
+import bjc.utils.gui.layout.HLayout;
+
+/**
+ * A simple input panel for a slider-controlled value and a manual-input field
+ * for setting the slider
+ *
+ * @author ben
+ *
+ */
+public class SliderInputPanel extends JPanel {
+ private final class NumberFormatter extends JFormattedTextField.AbstractFormatter {
+ private static final long serialVersionUID = -4448291795913908270L;
+
+ private final int minValue;
+ private final int maxValue;
+
+ private final int initValue;
+
+ public NumberFormatter(final SliderSettings settings) {
+ minValue = settings.minValue;
+ maxValue = settings.maxValue;
+
+ initValue = settings.initValue;
+ }
+
+ @Override
+ public Object stringToValue(final String text) throws ParseException {
+ try {
+ final int val = Integer.parseInt(text);
+
+ if (val < minValue)
+ throw new ParseException("Value must be greater than " + minValue, 0);
+ else if (val > maxValue)
+ throw new ParseException("Value must be smaller than " + maxValue, 0);
+ else return val;
+ } catch (final NumberFormatException nfex) {
+ final ParseException pex = new ParseException("Value must be a valid integer", 0);
+
+ pex.initCause(nfex);
+
+ throw pex;
+ }
+ }
+
+ @Override
+ public String valueToString(final Object value) throws ParseException {
+ if (value == null) return Integer.toString(initValue);
+
+ return Integer.toString((Integer) value);
+ }
+ }
+
+ /**
+ * Represents the settings for a slider
+ *
+ * @author ben
+ *
+ */
+ public static class SliderSettings {
+ /**
+ * The minimum value of the slider
+ */
+ public final int minValue;
+ /**
+ * The maximum value of the slider
+ */
+ public final int maxValue;
+
+ /**
+ * The initial value of the slider
+ */
+ public final int initValue;
+
+ /**
+ * Create a new slider settings, with the initial value in the
+ * middle
+ *
+ * @param min
+ * The minimum value of the slider
+ * @param max
+ * The maximum value of the slider
+ */
+ public SliderSettings(final int min, final int max) {
+ this(min, max, (min + max) / 2);
+ }
+
+ /**
+ * Create a new set of slider sttings
+ *
+ * @param min
+ * The minimum slider value
+ * @param max
+ * The maximum slider value
+ * @param init
+ * Th initial slider value
+ */
+ public SliderSettings(final int min, final int max, final int init) {
+ minValue = min;
+ maxValue = max;
+
+ initValue = init;
+ }
+ }
+
+ private static final long serialVersionUID = 2956394160569961404L;
+ private final JSlider slider;
+ private final JFormattedTextField field;
+
+ /**
+ * Create a new slider input panel
+ *
+ * @param lab
+ * The label for the field
+ * @param settings
+ * The settings for slider values
+ * @param majorTick
+ * The setting for where to place big ticks
+ * @param minorTick
+ * The setting for where to place small ticks
+ * @param action
+ * The action to execute for a given value
+ */
+ public SliderInputPanel(final String lab, final SliderSettings settings, final int majorTick,
+ final int minorTick, final Consumer<Integer> action) {
+ setLayout(new HLayout(3));
+
+ final JLabel label = new JLabel(lab);
+
+ slider = new JSlider(settings.minValue, settings.maxValue, settings.initValue);
+ field = new JFormattedTextField(new NumberFormatter(settings));
+
+ slider.setMajorTickSpacing(majorTick);
+ slider.setMinorTickSpacing(minorTick);
+ slider.setPaintTicks(true);
+ slider.setPaintLabels(true);
+
+ slider.addChangeListener((event) -> {
+ if (slider.getValueIsAdjusting()) {
+ // Do nothing
+ } else {
+ final int val = slider.getValue();
+
+ field.setValue(val);
+
+ action.accept(val);
+ }
+ });
+
+ field.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
+ field.setColumns(15);
+ field.addPropertyChangeListener("value", (event) -> {
+ final Object value = field.getValue();
+
+ if (value == null) {
+ // Do nothing
+ } else {
+ slider.setValue((Integer) value);
+ }
+ });
+
+ add(label);
+ add(slider);
+ add(field);
+ }
+
+ /**
+ * Reset the values in this panel to a specified value
+ *
+ * @param value
+ * The value to reset the fields to
+ */
+ public void resetValues(final int value) {
+ slider.setValue(value);
+
+ field.setValue(value);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/gui/panels/package-info.java b/base/src/main/java/bjc/utils/gui/panels/package-info.java
new file mode 100644
index 0000000..4361885
--- /dev/null
+++ b/base/src/main/java/bjc/utils/gui/panels/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * @author ben
+ *
+ */
+package bjc.utils.gui.panels; \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/ioutils/CLFormatter.java b/base/src/main/java/bjc/utils/ioutils/CLFormatter.java
new file mode 100644
index 0000000..eefd532
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/CLFormatter.java
@@ -0,0 +1,531 @@
+package bjc.utils.ioutils;
+
+import java.util.HashMap;
+import java.util.IllegalFormatConversionException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UnknownFormatConversionException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.PropertyDB;
+import bjc.utils.esodata.Tape;
+import bjc.utils.esodata.SingleTape;
+
+import static bjc.utils.PropertyDB.applyFormat;
+import static bjc.utils.PropertyDB.getCompiledRegex;
+import static bjc.utils.PropertyDB.getRegex;
+
+public class CLFormatter {
+ public static class CLModifiers {
+ public final boolean atMod;
+ public final boolean colonMod;
+
+ public CLModifiers(boolean at, boolean colon) {
+ atMod = at;
+ colonMod = colon;
+ }
+
+ public static CLModifiers fromString(String modString) {
+ boolean atMod = false;
+ boolean colonMod = false;
+ if(modString != null) {
+ atMod = modString.contains("@");
+ colonMod = modString.contains(":");
+ }
+
+ return new CLModifiers(atMod, colonMod);
+ }
+ }
+
+ public static class EscapeException extends RuntimeException {
+ public final boolean endIteration;
+
+ public EscapeException() {
+ endIteration = false;
+ }
+
+ public EscapeException(boolean end) {
+ endIteration = end;
+ }
+ }
+
+ @FunctionalInterface
+ public interface Directive {
+ /*
+ * @TODO fill in parameters
+ */
+ public void format();
+ }
+
+ private static final String prefixParam = getRegex("clFormatPrefix");
+ private static final Pattern pPrefixParam = Pattern.compile(prefixParam);
+
+ private static final String formatMod = getRegex("clFormatModifier");
+
+ private static final String prefixList = applyFormat("delimSeparatedList", prefixParam, ",");
+
+ private static final String directiveName = getRegex("clFormatName");
+
+ private static final String formatDirective = applyFormat("clFormatDirective", prefixList, formatMod, directiveName);
+ private static final Pattern pFormatDirective = Pattern.compile(formatDirective);
+
+ private Map<String, Directive> extraDirectives;
+
+ public CLFormatter() {
+ extraDirectives = new HashMap<>();
+ }
+
+ private void checkItem(Object itm, char directive) {
+ if(itm == null)
+ throw new IllegalArgumentException(String.format("No argument provided for %c directive", directive));
+ }
+
+ public String formatString(String format, Object... params) {
+ StringBuffer sb = new StringBuffer();
+ /* Put the parameters where we can easily handle them. */
+ Tape<Object> tParams = new SingleTape(params);
+
+ doFormatString(format, sb, tParams);
+
+ return sb.toString();
+ }
+
+ private void doFormatString(String format, StringBuffer sb, Tape<Object> tParams) {
+ Matcher dirMatcher = pFormatDirective.matcher(format);
+
+ while(dirMatcher.find()) {
+ dirMatcher.appendReplacement(sb, "");
+
+ String dirName = dirMatcher.group("name");
+ String dirFunc = dirMatcher.group("funcname");
+ String dirMods = dirMatcher.group("modifiers");
+ String dirParams = dirMatcher.group("params");
+
+ CLParameters arrParams = CLParameters.fromDirective(dirParams.split("(?<!'),"), tParams);
+ CLModifiers mods = CLModifiers.fromString(dirMods);
+
+ Object item = tParams.item();
+ if(dirName == null && dirFunc != null) {
+ /*
+ * @TODO implement user-called functions.
+ */
+ continue;
+ }
+
+ switch(dirName) {
+ case "A":
+ checkItem(item, 'A');
+ handleAestheticDirective(sb, item, mods, arrParams);
+ tParams.right();
+ break;
+ case "B":
+ checkItem(item, 'B');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('B', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 2);
+ tParams.right();
+ break;
+ case "C":
+ checkItem(item, 'C');
+ handleCDirective(sb, item, mods);
+ tParams.right();
+ break;
+ case "D":
+ checkItem(item, 'D');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('D', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 10);
+ tParams.right();
+ break;
+ case "O":
+ checkItem(item, 'O');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('O', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 8);
+ tParams.right();
+ break;
+ case "R":
+ checkItem(item, 'R');
+ handleRadixDirective(sb, mods, arrParams, item);
+ tParams.right();
+ break;
+ case "X":
+ checkItem(item, 'X');
+ if(!(item instanceof Number)) {
+ throw new IllegalFormatConversionException('X', item.getClass());
+ }
+ handleNumberDirective(sb, mods, arrParams, -1, ((Number)item).longValue(), 16);
+ tParams.right();
+ break;
+ case "&":
+ handleFreshlineDirective(sb, arrParams);
+ break;
+ case "%":
+ handleLiteralDirective(sb, arrParams, "\n", '%');
+ break;
+ case "|":
+ handleLiteralDirective(sb, arrParams, "\f", '|');
+ break;
+ case "~":
+ handleLiteralDirective(sb, arrParams, "~", '~');
+ break;
+ case "*":
+ handleGotoDirective(mods, arrParams, tParams);
+ break;
+ case "^":
+ handleEscapeDirective(mods, arrParams, tParams);
+ break;
+ case "[":
+ handleConditionalDirective(sb, mods, arrParams, tParams, dirMatcher);
+ break;
+ case "]":
+ throw new IllegalArgumentException("Found conditional-end outside of conditional.");
+ case ";":
+ throw new IllegalArgumentException("Found conditional-seperator outside of conditional.");
+ case "T":
+ case "<":
+ case ">":
+ /* @TODO
+ * Figure out how to implement
+ * tabulation/justification in a
+ * reasonable manner.
+ */
+ throw new IllegalArgumentException("Layout-control directives aren't implemented yet.");
+ case "F":
+ case "E":
+ case "G":
+ case "$":
+ /* @TODO implement floating point directives. */
+ throw new IllegalArgumentException("Floating-point directives aren't implemented yet.");
+ case "S":
+ case "W":
+ /* @TODO
+ * figure out if we want to implement
+ * someting for these directives instead
+ * of punting.
+ * */
+ throw new IllegalArgumentException("S and W aren't implemented. Use A instead");
+ default:
+ String msg = String.format("Unknown format directive '%s'", dirName);
+ throw new UnknownFormatConversionException(msg);
+ }
+ }
+
+ dirMatcher.appendTail(sb);
+ }
+
+ private void handleCDirective(StringBuffer buff, Object parm, CLModifiers mods) {
+ if(!(parm instanceof Character)) {
+ throw new IllegalFormatConversionException('C', parm.getClass());
+ }
+
+ char ch = (Character) parm;
+ int codepoint = (int) ch;
+
+ if(mods.colonMod) {
+ /*
+ * Colon mod means print Unicode character name.
+ */
+ buff.append(Character.getName(codepoint));
+ } else {
+ buff.append(ch);
+ }
+ }
+
+ private void handleFreshlineDirective(StringBuffer buff, CLParameters params) {
+ int nTimes = 1;
+
+ if(params.length() > 1) {
+ nTimes = params.getInt(0, "occurance count", '&');
+ }
+
+ if(buff.charAt(buff.length() - 1) == '\n') nTimes -= 1;
+
+ for(int i = 0; i < nTimes; i++) {
+ buff.append("\n");
+ }
+ }
+
+ private void handleLiteralDirective(StringBuffer buff, CLParameters params, String lit, char directive) {
+ int nTimes = 1;
+
+ if(params.length() > 1) {
+ nTimes = params.getInt(0, "occurance count", directive);
+ }
+
+ for(int i = 0; i < nTimes; i++) {
+ buff.append(lit);
+ }
+ }
+
+ private void handleNumberDirective(StringBuffer buff, CLModifiers mods, CLParameters params, int argidx, long val, int radix) {
+ /*
+ * Initialize the two padding related parameters, and
+ * then fill them in from the directive parameters if
+ * they are present.
+ */
+ int mincol = 0;
+ char padchar = ' ';
+ if(params.length() > (argidx + 2)) {
+ mincol = params.getIntDefault(argidx + 1, "minimum column count", 'R', 0);
+ }
+ if(params.length() > (argidx + 3)) {
+ padchar = params.getCharDefault(argidx + 2, "padding character", 'R', ' ');
+ }
+
+ if(mods.colonMod) {
+ /*
+ * We're doing commas, so check if the two
+ * comma-related parameters were supplied.
+ */
+ int commaInterval = 0;
+ char commaChar = ',';
+ if(params.length() > (argidx + 3)) {
+ commaChar = params.getCharDefault((argidx + 3), "comma character", 'R', ' ');
+ }
+ if(params.length() > (argidx + 4)) {
+ commaInterval = params.getIntDefault((argidx + 4), "comma interval", 'R', 0);
+ }
+
+ NumberUtils.toCommaString(val, mincol, padchar, commaInterval, commaChar, mods.atMod, radix);
+ } else {
+ NumberUtils.toNormalString(val, mincol, padchar, mods.atMod, radix);
+ }
+ }
+
+ private void handleRadixDirective(StringBuffer buff, CLModifiers mods, CLParameters params, Object arg) {
+ if(!(arg instanceof Number)) {
+ throw new IllegalFormatConversionException('R', arg.getClass());
+ }
+
+ /*
+ * @TODO see if this is the way we want to do this.
+ */
+ long val = ((Number)arg).longValue();
+
+ if(params.length() == 0) {
+ if(mods.atMod) {
+ buff.append(NumberUtils.toRoman((Long)val, mods.colonMod));
+ } else if(mods.colonMod) {
+ buff.append(NumberUtils.toOrdinal(val));
+ } else {
+ buff.append(NumberUtils.toCardinal(val));
+ }
+ } else {
+ if(params.length() < 1)
+ throw new IllegalArgumentException("R directive requires at least one parameter, the radix");
+
+ int radix = params.getInt(0, "radix", 'R');
+
+ handleNumberDirective(buff, mods, params, 0, val, radix);
+ }
+ }
+
+ private void handleAestheticDirective(StringBuffer buff, Object item, CLModifiers mods, CLParameters params) {
+ int mincol = 0, colinc = 1, minpad = 0;
+ char padchar = ' ';
+
+ if(params.length() > 1) {
+ mincol = params.getIntDefault(0, "minimum column count", 'A', 0);
+ }
+
+ if(params.length() < 4) {
+ throw new IllegalArgumentException("Must provide either zero, one or four arguments to A directive");
+ }
+
+ colinc = params.getIntDefault(1, "padding increment", 'A', 1);
+ minpad = params.getIntDefault(2, "minimum amount of padding", 'A', 0);
+ padchar = params.getCharDefault(3, "padding character", 'A', ' ');
+
+ StringBuilder work = new StringBuilder();
+
+ if(mods.atMod) {
+ for(int i = 0; i < minpad; i++) {
+ work.append(padchar);
+ }
+
+ for(int i = work.length(); i < mincol; i++) {
+ for(int k = 0; k < colinc; k++) {
+ work.append(padchar);
+ }
+ }
+ }
+
+ work.append(item.toString());
+
+ if(!mods.atMod) {
+ for(int i = 0; i < minpad; i++) {
+ work.append(padchar);
+ }
+
+ for(int i = work.length(); i < mincol; i++) {
+ for(int k = 0; k < colinc; k++) {
+ work.append(padchar);
+ }
+ }
+ }
+ }
+
+ private void handleGotoDirective(CLModifiers mods, CLParameters params, Tape<Object> formatParams) {
+ if(mods.colonMod) {
+ int num = 1;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "number of arguments backward", '*', 1);
+ }
+
+ formatParams.left(num);
+ } else if(mods.atMod) {
+ int num = 0;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "argument index", '*', 0);
+ }
+
+ formatParams.first();
+ formatParams.right(num);
+ } else {
+ int num = 1;
+ if(params.length() > 1) {
+ num = params.getIntDefault(0, "number of arguments forward", '*', 1);
+ }
+
+ formatParams.right(num);
+ }
+ }
+
+ private void handleConditionalDirective(StringBuffer sb, CLModifiers mods, CLParameters arrParams, Tape<Object> formatParams, Matcher dirMatcher) {
+ StringBuffer condBody = new StringBuffer();
+
+ List<String> clauses = new ArrayList<>();
+ String defClause = null;
+ boolean isDefault = false;
+
+ while(dirMatcher.find()) {
+ /* Process a list of clauses. */
+ String dirName = dirMatcher.group("name");
+ String dirMods = dirMatcher.group("modifiers");
+
+ if(dirName != null) {
+ /* Append everything up to this directive. */
+ dirMatcher.appendReplacement(condBody, "");
+
+ if(dirName.equals("]")) {
+ /* End the conditional. */
+ String clause = condBody.toString();
+ if(isDefault) {
+ defClause = clause;
+ } else {
+ clauses.add(clause);
+ }
+
+ break;
+ } else if(dirName.equals(";")) {
+ /* End the clause. */
+ String clause = condBody.toString();
+ if(isDefault) {
+ defClause = clause;
+ } else {
+ clauses.add(clause);
+ }
+
+ /* Mark the next clause as the default. */
+ if(dirMods.contains(":")) {
+ isDefault = true;
+ }
+ } else {
+ /* Not a special directive. */
+ condBody.append(dirMatcher.group());
+ }
+ }
+ }
+
+ Object par = formatParams.item();
+ if(mods.colonMod) {
+ formatParams.right();
+
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Boolean)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ boolean res = (Boolean)par;
+
+ String fmt;
+ if(res) fmt = clauses.get(1);
+ else fmt = clauses.get(0);
+
+ doFormatString(fmt, sb, formatParams);
+ } else if(mods.atMod) {
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Boolean)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ boolean res = (Boolean)par;
+
+ if(res) {
+ doFormatString(clauses.get(0), sb, formatParams);
+ } else {
+ formatParams.right();
+ }
+ } else {
+ int res;
+ if(arrParams.length() > 1) {
+ res = arrParams.getInt(0, "conditional choice", '[');
+ } else {
+ if(par == null) {
+ throw new IllegalArgumentException("No parameter provided for [ directive.");
+ } else if(!(par instanceof Number)) {
+ throw new IllegalFormatConversionException('[', par.getClass());
+ }
+ res = ((Number)par).intValue();
+
+ formatParams.right();
+ }
+
+ if(res < 0 || res > clauses.size()) {
+ if(defClause != null) doFormatString(defClause, sb, formatParams);
+ } else {
+ doFormatString(clauses.get(res), sb, formatParams);
+ }
+ }
+ return;
+ }
+
+ private void handleEscapeDirective(CLModifiers mods, CLParameters params, Tape<Object> formatParams) {
+ boolean shouldExit;
+
+ switch(params.length()) {
+ case 0:
+ shouldExit = formatParams.size() == 0;
+ break;
+ case 1:
+ int num = params.getInt(0, "condition count", '^');
+ shouldExit = num == 0;
+ break;
+ case 2:
+ int left = params.getInt(0, "left-hand condition", '^');
+ int right = params.getInt(1, "right-hand condition", '^');
+ shouldExit = left == right;
+ break;
+ case 3:
+ default:
+ int low = params.getInt(0, "lower-bound condition", '^');
+ int mid = params.getInt(1, "interval condition", '^');
+ int high = params.getInt(2, "upper-bound condition", '^');
+ shouldExit = (low <= mid) && (mid <= high);
+ break;
+ }
+
+ /* At negates it. */
+ if(mods.atMod) shouldExit = !shouldExit;
+
+ if(shouldExit) throw new EscapeException(mods.colonMod);
+ }
+
+
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/CLParameters.java b/base/src/main/java/bjc/utils/ioutils/CLParameters.java
new file mode 100644
index 0000000..e4bb6fb
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/CLParameters.java
@@ -0,0 +1,109 @@
+package bjc.utils.ioutils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import bjc.utils.esodata.Tape;
+
+/**
+ * Represents a set of parameters to a CL format directive.
+ *
+ * @author Benjamin Culkin
+ */
+public class CLParameters {
+ private String[] params;
+
+ public CLParameters(String[] params) {
+ this.params = params;
+ }
+
+ public int length() {
+ return params.length;
+ }
+
+ /**
+ * Creates a set of parameters from an array of parameters.
+ *
+ * Mostly, this just fills in V and # parameters.
+ *
+ * @param params
+ * The parameters of the directive.
+ * @param dirParams
+ * The parameters of the format string.
+ *
+ * @return A set of CL parameters.
+ */
+ public static CLParameters fromDirective(String[] params, Tape<Object> dirParams) {
+ List<String> parameters = new ArrayList<>();
+
+ for(String param : params) {
+ if(param.equalsIgnoreCase("V")) {
+ Object par = dirParams.item();
+ boolean succ = dirParams.right();
+
+ if(par == null) {
+ throw new IllegalArgumentException("Expected a format parameter for V inline parameter");
+ }
+
+ if(par instanceof Number) {
+ int val = ((Number)par).intValue();
+
+ parameters.add(Integer.toString(val));
+ } else if(par instanceof Character) {
+ char ch = ((Character)par);
+
+ parameters.add(Character.toString(ch));
+ } else {
+ throw new IllegalArgumentException("Incorrect type of parameter for V inline parameter");
+ }
+ } else if(param.equals("#")) {
+ parameters.add(Integer.toString(dirParams.position()));
+ } else {
+ parameters.add(param);
+ }
+ }
+
+ return new CLParameters(parameters.toArray(new String[0]));
+ }
+
+ public char getCharDefault(int idx, String paramName, char directive, char def) {
+ if(!params[idx].equals("")) {
+ return getChar(idx, paramName, directive);
+ }
+
+ return def;
+ }
+
+ public char getChar(int idx, String paramName, char directive) {
+ String param = params[idx];
+
+ if(!param.startsWith("'")) {
+ throw new IllegalArgumentException(String.format("Invalid %s %s to %c directive", paramName, param, directive));
+ }
+
+ return param.charAt(1);
+ }
+
+ public int getIntDefault(int idx, String paramName, char directive, int def) {
+ if(!params[idx].equals("")) {
+
+ }
+
+ return def;
+ }
+
+ public int getInt(int idx, String paramName, char directive) {
+ String param = params[idx];
+
+ try {
+ return Integer.parseInt(param);
+ } catch (NumberFormatException nfex) {
+ String msg = String.format("Invalid %s %s to %c directive", paramName, param, directive);
+
+ IllegalArgumentException iaex = new IllegalArgumentException(msg);
+ iaex.initCause(nfex);
+
+ throw iaex;
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/NumberUtils.java b/base/src/main/java/bjc/utils/ioutils/NumberUtils.java
new file mode 100644
index 0000000..1b754e2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/NumberUtils.java
@@ -0,0 +1,405 @@
+package bjc.utils.ioutils;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.LongPredicate;
+
+import static java.util.Map.Entry;
+
+public class NumberUtils {
+ /*
+ * @TODO Use U+305 for large roman numerals, as well as excels 'concise'
+ * numerals (as implemented by roman()).
+ */
+ public static String toRoman(long number, boolean classic) {
+ StringBuilder work = new StringBuilder();
+
+ long currNumber = number;
+
+ if(currNumber == 0) {
+ return "N";
+ }
+
+ if(currNumber < 0) {
+ currNumber *= -1;
+
+ work.append("-");
+ }
+
+ if(currNumber >= 1000) {
+ int numM = (int)(currNumber / 1000);
+ currNumber = currNumber % 1000;
+
+ for(int i = 0; i < numM; i++) {
+ work.append("M");
+ }
+ }
+
+ if(currNumber >= 900 && !classic) {
+ currNumber = currNumber % 900;
+
+ work.append("CM");
+ }
+
+ if(currNumber >= 500) {
+ currNumber = currNumber % 500;
+
+ work.append("D");
+ }
+
+ if(currNumber >= 400 && !classic) {
+ currNumber = currNumber % 400;
+
+ work.append("CD");
+ }
+
+ if(currNumber >= 100) {
+ int numC = (int)(currNumber / 100);
+ currNumber = currNumber % 100;
+
+ for(int i = 0; i < numC; i++) {
+ work.append("C");
+ }
+ }
+
+ if(currNumber >= 90 && !classic) {
+ currNumber = currNumber % 90;
+
+ work.append("XC");
+ }
+
+ if(currNumber >= 50) {
+ currNumber = currNumber % 50;
+
+ work.append("L");
+ }
+
+ if(currNumber >= 40 && !classic) {
+ currNumber = currNumber % 40;
+
+ work.append("XL");
+ }
+
+ if(currNumber >= 10) {
+ int numX = (int)(currNumber / 10);
+ currNumber = currNumber % 10;
+
+ for(int i = 0; i < numX; i++) {
+ work.append("X");
+ }
+ }
+
+ if(currNumber >= 9 && !classic) {
+ currNumber = currNumber % 9;
+
+ work.append("IX");
+ }
+
+ if(currNumber >= 5) {
+ currNumber = currNumber % 5;
+
+ work.append("V");
+ }
+
+ if(currNumber >= 4 && !classic) {
+ currNumber = currNumber % 4;
+
+ work.append("IV");
+ }
+
+ if(currNumber >= 1) {
+ int numI = (int)(currNumber / 1);
+ currNumber = currNumber % 1;
+
+ for(int i = 0; i < numI; i++) {
+ work.append("I");
+ }
+ }
+
+ return work.toString();
+ }
+
+ public static String toCardinal(long number) {
+ return toCardinal(number, null);
+ }
+
+ private static String[] cardinals = new String[] {
+ "zero", "one", "two", "three", "four", "five", "six", "seven",
+ "eight", "nine", "ten", "eleven", "twelve", "thirteen",
+ "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
+ "nineteen", "twenty",
+ };
+
+ public static class CardinalState {
+ public final Map<Long, String> customNumbers;
+ public final Map<LongPredicate, BiFunction<Long, CardinalState, String>> customScales;
+
+ public CardinalState(Map<Long, String> customNumbers, Map<LongPredicate, BiFunction<Long, CardinalState, String>> customScales) {
+ this.customNumbers = customNumbers;
+ this.customScales = customScales;
+ }
+
+ public String handleCustom(long number) {
+ if(customNumbers.containsKey(number)) {
+ return customNumbers.get(number);
+ }
+
+ for(Entry<LongPredicate, BiFunction<Long, CardinalState, String>> ent : customScales.entrySet()) {
+ if(ent.getKey().test(number)) {
+ return ent.getValue().apply(number, this);
+ }
+ }
+
+ return null;
+ }
+ }
+
+ public static String toCardinal(long number, CardinalState custom) {
+ if(custom != null) {
+ String res = custom.handleCustom(number);
+
+ if(res != null) return res;
+ }
+
+ if(number < 0) return "negative " + toCardinal(number * -1, custom);
+
+ if(number <= 20) return cardinals[(int)number];
+
+ if(number < 100) {
+ if(number % 10 == 0) {
+ switch((int)number) {
+ case 30:
+ return "thirty";
+ case 40:
+ return "forty";
+ case 50:
+ return "fifty";
+ case 60:
+ return "sixty";
+ case 70:
+ return "seventy";
+ case 80:
+ return "eighty";
+ case 90:
+ return "ninety";
+ default:
+ /*
+ * Shouldn't happen.
+ */
+ assert(false);
+ }
+ }
+
+ long numTens = (long)(number / 10);
+ long numOnes = number % 10;
+
+ return toCardinal(numTens, custom) + "-" + toCardinal(numOnes, custom);
+ }
+
+ if(number < 1000) {
+ long numHundreds = (long)(number / 100);
+ long rest = number % 100;
+
+ return toCardinal(numHundreds, custom) + " hundred and " + toCardinal(rest, custom);
+ }
+
+ long MILLION = (long)(Math.pow(10, 6));
+ if(number < MILLION) {
+ long numThousands = (long)(number / 1000);
+ long rest = number % 1000;
+
+ return toCardinal(numThousands, custom) + " thousand, " + toCardinal(rest, custom);
+ }
+
+ long BILLION = (long)(Math.pow(10, 9));
+ if(number < BILLION) {
+ long numMillions = (long)(number / MILLION);
+ long rest = number % MILLION;
+
+ return toCardinal(numMillions, custom) + " million, " + toCardinal(rest, custom);
+ }
+
+ long TRILLION = (long)(Math.pow(10, 12));
+ if(number < TRILLION) {
+ long numBillions = (long)(number / BILLION);
+ long rest = number % BILLION;
+
+ return toCardinal(numBillions, custom) + " billion, " + toCardinal(rest, custom);
+ }
+
+ throw new IllegalArgumentException("Numbers greater than or equal to 1 trillion are not supported yet.");
+ }
+
+ public static String toOrdinal(long number) {
+ if(number < 0) {
+ return "minus " + toOrdinal(number);
+ }
+
+ if(number < 20) {
+ switch((int)number) {
+ case 0:
+ return "zeroth";
+ case 1:
+ return "first";
+ case 2:
+ return "second";
+ case 3:
+ return "third";
+ case 4:
+ return "fourth";
+ case 5:
+ return "fifth";
+ case 6:
+ return "sixth";
+ case 7:
+ return "seventh";
+ case 8:
+ return "eighth";
+ case 9:
+ return "ninth";
+ case 10:
+ return "tenth";
+ case 11:
+ return "eleventh";
+ case 12:
+ return "twelfth";
+ case 13:
+ return "thirteenth";
+ case 14:
+ return "fourteenth";
+ case 15:
+ return "fifteenth";
+ case 16:
+ return "sixteenth";
+ case 17:
+ return "seventeenth";
+ case 18:
+ return "eighteenth";
+ case 19:
+ return "nineteenth";
+ default:
+ /*
+ * Shouldn't happen.
+ */
+ assert(false);
+ }
+ }
+
+ if(number < 100) {
+ if(number % 10 == 0) {
+ switch((int)number) {
+ case 20:
+ return "twentieth";
+ case 30:
+ return "thirtieth";
+ case 40:
+ return "fortieth";
+ case 50:
+ return "fiftieth";
+ case 60:
+ return "sixtieth";
+ case 70:
+ return "seventieth";
+ case 80:
+ return "eightieth";
+ case 90:
+ return "ninetieth";
+ }
+ }
+
+ long numPostfix = number % 10;
+ return toCardinal(number - numPostfix) + "-" + toOrdinal(numPostfix);
+ }
+
+ long procNum = number % 100;
+ long tens = (long)(procNum / 10);
+ long ones = procNum % 10;
+
+ if(tens == 1) {
+ return Long.toString(number) + "th";
+ }
+
+ switch((int)ones) {
+ case 1:
+ return Long.toString(number) + "st";
+ case 2:
+ return Long.toString(number) + "nd";
+ case 3:
+ return Long.toString(number) + "rd";
+ default:
+ return Long.toString(number) + "th";
+ }
+ }
+
+ private static char[] radixChars = new char[62];
+ static {
+ int idx = 0;
+
+ for(char i = 0; i < 10; i++) {
+ radixChars[idx] = (char)('0' + i);
+
+ idx += 1;
+ }
+
+ for(char i = 0; i < 26; i++) {
+ radixChars[idx] = (char)('A' + i);
+
+ idx += 1;
+ }
+
+ for(char i = 0; i < 26; i++) {
+ radixChars[idx] = (char)('a' + i);
+
+ idx += 1;
+ }
+ }
+
+ public static String toCommaString(long val, int mincols, char padchar, int commaInterval, char commaChar, boolean signed, int radix) {
+ if(radix > radixChars.length) {
+ throw new IllegalArgumentException(String.format("Radix %d is larger than largest supported radix %d", radix, radixChars.length));
+ }
+
+ StringBuilder work = new StringBuilder();
+
+ boolean isNeg = false;
+ long currVal = val;
+ if(currVal < 0) {
+ isNeg = true;
+ currVal *= -1;
+ }
+
+ if(currVal == 0) {
+ work.append(radixChars[0]);
+ } else {
+ int valCounter = 0;
+
+ while(currVal != 0) {
+ valCounter += 1;
+
+ int radDigit = (int)(currVal % radix);
+ work.append(radixChars[radDigit]);
+ currVal = (long)(currVal / radix);
+
+ if(commaInterval != 0 && valCounter % commaInterval == 0) work.append(commaChar);
+ }
+ }
+
+ if(isNeg) work.append("-");
+ else if(signed) work.append("+");
+
+ work.reverse();
+
+ /* @TODO Should we have some way to specify how to pad? */
+ if(work.length() < mincols) {
+ for(int i = work.length(); i < mincols; i++) {
+ work.append(padchar);
+ }
+ }
+
+ return work.toString();
+ }
+
+ public static String toNormalString(long val, int mincols, char padchar, boolean signed, int radix) {
+ return toCommaString(val, mincols, padchar, 0, ',', signed, radix);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/Prompter.java b/base/src/main/java/bjc/utils/ioutils/Prompter.java
new file mode 100644
index 0000000..a6ec4c0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/Prompter.java
@@ -0,0 +1,47 @@
+package bjc.utils.ioutils;
+
+import java.io.PrintStream;
+
+import bjc.utils.ioutils.blocks.TriggeredBlockReader;
+
+/**
+ * A runnable for use with {@link TriggeredBlockReader} to prompt the user for
+ * input.
+ *
+ * @author bjculkin
+ *
+ */
+public final class Prompter implements Runnable {
+ private String promt;
+ private final PrintStream printer;
+
+ /**
+ * Create a new prompter using the specified prompt.
+ *
+ * @param prompt
+ * The prompt to present.
+ *
+ * @param output
+ * The stream to print the prompt on.
+ */
+ public Prompter(final String prompt, final PrintStream output) {
+ promt = prompt;
+
+ printer = output;
+ }
+
+ /**
+ * Set the prompt this prompter uses.
+ *
+ * @param prompt
+ * The prompt this prompter uses.
+ */
+ public void setPrompt(final String prompt) {
+ promt = prompt;
+ }
+
+ @Override
+ public void run() {
+ printer.print(promt);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java b/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java
new file mode 100644
index 0000000..71f6782
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/RegexStringEditor.java
@@ -0,0 +1,230 @@
+package bjc.utils.ioutils;
+
+import java.util.function.BiFunction;
+import java.util.function.UnaryOperator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.data.Toggle;
+import bjc.utils.data.ValueToggle;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+import bjc.utils.functypes.ID;
+
+/**
+ * Editor methods for strings based off the command language for the Sam editor.
+ *
+ * @author EVE
+ *
+ */
+public class RegexStringEditor {
+ private static final UnaryOperator<String> SID = ID.id();
+
+ /**
+ * Replace every occurrence of the pattern with the result of applying
+ * the action to the string matched by the pattern.
+ *
+ * @param input
+ * The input string to process.
+ *
+ * @param patt
+ * The pattern to match the string against.
+ *
+ * @param action
+ * The action to transform matches with.
+ *
+ * @return The string, with matches replaced with the action.
+ */
+ public static String onOccurances(final String input, final Pattern patt, final UnaryOperator<String> action) {
+ return reduceOccurances(input, patt, SID, action);
+ }
+
+ /**
+ * Replace every occurrence between the patterns with the result of
+ * applying the action to the strings between the patterns.
+ *
+ * @param input
+ * The input string to process.
+ *
+ * @param patt
+ * The pattern to match the string against.
+ *
+ * @param action
+ * The action to transform matches with.
+ *
+ * @return The string, with strings between the matches replaced with
+ * the action.
+ */
+ public static String betweenOccurances(final String input, final Pattern patt,
+ final UnaryOperator<String> action) {
+ return reduceOccurances(input, patt, action, SID);
+ }
+
+ /**
+ * Execute actions between and on matches of a regular expression.
+ *
+ * @param input
+ * The input string.
+ *
+ * @param rPatt
+ * The pattern to match against the string.
+ *
+ * @param betweenAction
+ * The function to execute between matches of the string.
+ *
+ * @param onAction
+ * The function to execute on matches of the string.
+ *
+ * @return The string, with both actions applied.
+ */
+ public static String reduceOccurances(final String input, final Pattern rPatt,
+ final UnaryOperator<String> betweenAction, final UnaryOperator<String> onAction) {
+ /*
+ * Get all of the occurances.
+ */
+ final IList<String> occurances = listOccurances(input, rPatt);
+
+ /*
+ * Execute the correct action on every occurance.
+ */
+ final Toggle<UnaryOperator<String>> actions = new ValueToggle<>(onAction, betweenAction);
+ final BiFunction<String, StringBuilder, StringBuilder> reducer = (strang, state) -> {
+ return state.append(actions.get().apply(strang));
+ };
+
+ /*
+ * Convert the list back to a string.
+ */
+ return occurances.reduceAux(new StringBuilder(), reducer, StringBuilder::toString);
+ }
+
+ /**
+ * Execute actions between and on matches of a regular expression.
+ *
+ * @param input
+ * The input string.
+ *
+ * @param rPatt
+ * The pattern to match against the string.
+ *
+ * @param betweenAction
+ * The function to execute between matches of the string.
+ *
+ * @param onAction
+ * The function to execute on matches of the string.
+ *
+ * @return The string, with both actions applied.
+ */
+ public static IList<String> mapOccurances(final String input, final Pattern rPatt,
+ final UnaryOperator<String> betweenAction, final UnaryOperator<String> onAction) {
+ /*
+ * Get all of the occurances.
+ */
+ final IList<String> occurances = listOccurances(input, rPatt);
+
+ /*
+ * Execute the correct action on every occurance.
+ */
+ final Toggle<UnaryOperator<String>> actions = new ValueToggle<>(onAction, betweenAction);
+ return occurances.map(strang -> actions.get().apply(strang));
+ }
+
+ /**
+ * Separate a string into match/non-match segments.
+ *
+ * @param input
+ * The string to separate.
+ *
+ * @param rPatt
+ * The pattern to use for separation.
+ *
+ * @return The string, as a list of match/non-match segments,
+ * starting/ending with a non-match segment.
+ */
+ public static IList<String> listOccurances(final String input, final Pattern rPatt) {
+ final IList<String> res = new FunctionalList<>();
+
+ /*
+ * Create the matcher and work buffer.
+ */
+ final Matcher matcher = rPatt.matcher(input);
+ StringBuffer work = new StringBuffer();
+
+ /*
+ * For every match.
+ */
+ while (matcher.find()) {
+ final String match = matcher.group();
+
+ /*
+ * Append the text until the match to the buffer.
+ */
+ matcher.appendReplacement(work, "");
+
+ res.add(work.toString());
+ res.add(match);
+
+ /*
+ * Clear the buffer.
+ */
+ work = new StringBuffer();
+ }
+
+ /*
+ * Add the text after the last match to the buffer.
+ */
+ matcher.appendTail(work);
+ res.add(work.toString());
+
+ return res;
+ }
+
+ /**
+ * Apply an operation to a string if it matches a regular expression.
+ *
+ * @param input
+ * The input string.
+ *
+ * @param patt
+ * The pattern to match against it.
+ *
+ * @param action
+ * The action to execute if it matches.
+ *
+ * @return The string, modified by the action if the pattern matched.
+ */
+ public static String ifMatches(final String input, final Pattern patt, final UnaryOperator<String> action) {
+ final Matcher matcher = patt.matcher(input);
+
+ if (matcher.matches()) {
+ return action.apply(input);
+ } else {
+ return input;
+ }
+ }
+
+ /**
+ * Apply an operation to a string if it matches a regular expression.
+ *
+ * @param input
+ * The input string.
+ *
+ * @param patt
+ * The pattern to match against it.
+ *
+ * @param action
+ * The action to execute if it doesn't match.
+ *
+ * @return The string, modified by the action if the pattern didn't
+ * match.
+ */
+ public static String ifNotMatches(final String input, final Pattern patt, final UnaryOperator<String> action) {
+ final Matcher matcher = patt.matcher(input);
+
+ if (matcher.matches()) {
+ return input;
+ } else {
+ return action.apply(input);
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java
new file mode 100644
index 0000000..7c5205b
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/RuleBasedConfigReader.java
@@ -0,0 +1,265 @@
+package bjc.utils.ioutils;
+
+import java.io.InputStream;
+import java.util.InputMismatchException;
+import java.util.Scanner;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.Identity;
+import bjc.utils.data.Pair;
+import bjc.utils.exceptions.UnknownPragmaException;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.FunctionalStringTokenizer;
+import bjc.utils.funcdata.IMap;
+
+/**
+ * This class parses a rules based config file, and uses it to drive a provided
+ * set of actions
+ *
+ * @author ben
+ *
+ * @param <E>
+ * The type of the state object to use
+ *
+ */
+public class RuleBasedConfigReader<E> {
+ /* Function to execute when starting a rule.
+ * Takes the tokenizer, and a pair of the read token and application state
+ */
+ private BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start;
+
+ /*
+ * Function to use when continuing a rule
+ * Takes a tokenizer and application state
+ */
+ private BiConsumer<FunctionalStringTokenizer, E> continueRule;
+
+ /*
+ * Function to use when ending a rule
+ * Takes an application state
+ */
+ private Consumer<E> end;
+
+ /*
+ * Map of pragma names to pragma actions
+ * Pragma actions are functions taking a tokenizer and application state
+ */
+ private final IMap<String, BiConsumer<FunctionalStringTokenizer, E>> pragmas;
+
+ /**
+ * Create a new rule-based config reader
+ *
+ * @param start
+ * The action to fire when starting a rule
+ * @param continueRule
+ * The action to fire when continuing a rule
+ * @param end
+ * The action to fire when ending a rule
+ */
+ public RuleBasedConfigReader(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start,
+ final BiConsumer<FunctionalStringTokenizer, E> continueRule, final Consumer<E> end) {
+ this.start = start;
+ this.continueRule = continueRule;
+ this.end = end;
+
+ this.pragmas = new FunctionalMap<>();
+ }
+
+ /**
+ * Add a pragma to this reader
+ *
+ * @param name
+ * The name of the pragma to add
+ * @param action
+ * The function to execute when this pragma is read
+ */
+ public void addPragma(final String name, final BiConsumer<FunctionalStringTokenizer, E> action) {
+ if (name == null) throw new NullPointerException("Pragma name must not be null");
+ else if (action == null) throw new NullPointerException("Pragma action must not be null");
+
+ pragmas.put(name, action);
+ }
+
+ private void continueRule(final E state, final boolean isRuleOpen, final String line) {
+ // Make sure our input is correct
+ if (isRuleOpen == false)
+ throw new InputMismatchException("Cannot continue rule with no rule open");
+ else if (continueRule == null)
+ throw new InputMismatchException("Rule continuation not supported for current grammar");
+
+ /*
+ * Accept the rule
+ */
+ continueRule.accept(new FunctionalStringTokenizer(line.substring(1), " "), state);
+ }
+
+ private boolean endRule(final E state, final boolean isRuleOpen) {
+ /*
+ * Ignore blank line without an open rule
+ */
+ if (isRuleOpen == false)
+ /*
+ * Do nothing
+ */
+ return false;
+ else {
+ /*
+ * Nothing happens on rule end
+ */
+ if (end != null) {
+ /*
+ * Process the rule ending
+ */
+ end.accept(state);
+ }
+
+ /*
+ * Return a closed rule
+ */
+ return false;
+ }
+ }
+
+ /**
+ * Run a stream through this reader
+ *
+ * @param input
+ * The stream to get input
+ * @param initialState
+ * The initial state of the reader
+ * @return The final state of the reader
+ */
+ public E fromStream(final InputStream input, final E initialState) {
+ if (input == null) throw new NullPointerException("Input stream must not be null");
+
+ /*
+ * Application state: We're giving this back later
+ */
+ final E state = initialState;
+
+ /*
+ * Prepare our input source
+ */
+ try (Scanner source = new Scanner(input)) {
+ source.useDelimiter("\n");
+ /*
+ * This is true when a rule's open
+ */
+ final IHolder<Boolean> isRuleOpen = new Identity<>(false);
+
+ /*
+ * Do something for every line of the file
+ */
+ source.forEachRemaining((line) -> {
+ /*
+ * Skip comment lines
+ */
+ if (line.startsWith("#") || line.startsWith("//"))
+ /*
+ * It's a comment
+ */
+ return;
+ else if (line.equals("")) {
+ /*
+ * End the rule
+ */
+ isRuleOpen.replace(endRule(state, isRuleOpen.getValue()));
+ } else if (line.startsWith("\t")) {
+ /*
+ * Continue the rule
+ */
+ continueRule(state, isRuleOpen.getValue(), line);
+ } else {
+ /*
+ * Open a rule
+ */
+ isRuleOpen.replace(startRule(state, isRuleOpen.getValue(), line));
+ }
+ });
+ }
+
+ /*
+ * Return the state that the user has created
+ */
+ return state;
+ }
+
+ /**
+ * Set the action to execute when continuing a rule
+ *
+ * @param continueRule
+ * The action to execute on continuation of a rule
+ */
+ public void setContinueRule(final BiConsumer<FunctionalStringTokenizer, E> continueRule) {
+ this.continueRule = continueRule;
+ }
+
+ /**
+ * Set the action to execute when ending a rule
+ *
+ * @param end
+ * The action to execute on ending of a rule
+ */
+ public void setEndRule(final Consumer<E> end) {
+ this.end = end;
+ }
+
+ /**
+ * Set the action to execute when starting a rule
+ *
+ * @param start
+ * The action to execute on starting of a rule
+ */
+ public void setStartRule(final BiConsumer<FunctionalStringTokenizer, IPair<String, E>> start) {
+ if (start == null) throw new NullPointerException("Action on rule start must be non-null");
+
+ this.start = start;
+ }
+
+ private boolean startRule(final E state, boolean isRuleOpen, final String line) {
+ /*
+ * Create the line tokenizer
+ */
+ final FunctionalStringTokenizer tokenizer = new FunctionalStringTokenizer(line, " ");
+
+ /*
+ * Get the initial token
+ */
+ final String nextToken = tokenizer.nextToken();
+
+ /*
+ * Handle pragmas
+ */
+ if (nextToken.equals("pragma")) {
+ /*
+ * Get the pragma name
+ */
+ final String token = tokenizer.nextToken();
+
+ /*
+ * Handle pragmas
+ */
+ pragmas.getOrDefault(token, (tokenzer, stat) -> {
+ throw new UnknownPragmaException("Unknown pragma " + token);
+ }).accept(tokenizer, state);
+ } else {
+ /*
+ * Make sure input is correct
+ */
+ if (isRuleOpen == true)
+ throw new InputMismatchException("Nested rules are currently not supported");
+
+ /*
+ * Start a rule
+ */
+ start.accept(tokenizer, new Pair<>(nextToken, state));
+
+ isRuleOpen = true;
+ }
+
+ return isRuleOpen;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java b/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java
new file mode 100644
index 0000000..e26a7ee
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/RuleBasedReaderPragmas.java
@@ -0,0 +1,100 @@
+package bjc.utils.ioutils;
+
+import java.util.function.BiConsumer;
+
+import bjc.utils.exceptions.PragmaFormatException;
+import bjc.utils.funcdata.FunctionalStringTokenizer;
+import bjc.utils.funcutils.ListUtils;
+
+/**
+ * Contains factory methods for common pragma types
+ *
+ * @author ben
+ *
+ */
+public class RuleBasedReaderPragmas {
+
+ /**
+ * Creates a pragma that takes a single integer argument
+ *
+ * @param <StateType>
+ * The type of state that goes along with this pragma
+ * @param name
+ * The name of this pragma, for error message purpose
+ * @param consumer
+ * The function to invoke with the parsed integer
+ * @return A pragma that functions as described above.
+ */
+ public static <StateType> BiConsumer<FunctionalStringTokenizer, StateType> buildInteger(final String name,
+ final BiConsumer<Integer, StateType> consumer) {
+ return (tokenizer, state) -> {
+ /*
+ * Check our input is correct
+ */
+ if (!tokenizer.hasMoreTokens()) {
+ String fmt = "Pragma %s requires one integer argument";
+
+ throw new PragmaFormatException(String.format(fmt, name));
+ }
+
+ /*
+ * Read the argument
+ */
+ final String token = tokenizer.nextToken();
+
+ try {
+ /*
+ * Run the pragma
+ */
+ consumer.accept(Integer.parseInt(token), state);
+ } catch (final NumberFormatException nfex) {
+ /*
+ * Tell the user their argument isn't correct
+ */
+ String fmt = "Argument %s to %s pragma isn't a valid integer, and this pragma requires an integer argument.";
+
+ final PragmaFormatException pfex = new PragmaFormatException(String.format(fmt, token, name));
+
+ pfex.initCause(nfex);
+
+ throw pfex;
+ }
+ };
+ }
+
+ /**
+ * Creates a pragma that takes any number of arguments and collapses
+ * them all into a single string
+ *
+ * @param <StateType>
+ * The type of state that goes along with this pragma
+ * @param name
+ * The name of this pragma, for error message purpose
+ * @param consumer
+ * The function to invoke with the parsed string
+ * @return A pragma that functions as described above.
+ */
+ public static <StateType> BiConsumer<FunctionalStringTokenizer, StateType> buildStringCollapser(
+ final String name, final BiConsumer<String, StateType> consumer) {
+ return (tokenizer, state) -> {
+ /*
+ * Check our input
+ */
+ if (!tokenizer.hasMoreTokens()) {
+ String fmt = "Pragma %s requires one or more string arguments.";
+
+ throw new PragmaFormatException(String.format(fmt, name));
+ }
+
+ /*
+ * Build our argument
+ */
+ final String collapsed = ListUtils.collapseTokens(tokenizer.toList());
+
+ /*
+ * Run the pragma
+ */
+ consumer.accept(collapsed, state);
+ };
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java b/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java
new file mode 100644
index 0000000..e6279c4
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/SimpleProperties.java
@@ -0,0 +1,170 @@
+package bjc.utils.ioutils;
+
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+import java.util.Set;
+
+/**
+ * Simple file based properties.
+ *
+ * @author EVE
+ *
+ */
+public class SimpleProperties implements Map<String, String> {
+ private final Map<String, String> props;
+
+ /**
+ * Create a new set of simple properties.
+ */
+ public SimpleProperties() {
+ props = new HashMap<>();
+ }
+
+ /**
+ * Load properties from the provided input stream.
+ *
+ * The format is the name, a space, then the body.
+ *
+ * All leading/trailing spaces from the name & body are removed.
+ *
+ * @param is
+ * The stream to read from.
+ *
+ * @param allowDuplicates
+ * Whether or not duplicate keys should be allowed.
+ */
+ public void loadFrom(final InputStream is, final boolean allowDuplicates) {
+ try (Scanner scn = new Scanner(is)) {
+ while (scn.hasNextLine()) {
+ final String ln = scn.nextLine().trim();
+
+ /*
+ * Skip blank lines/comments
+ */
+ if (ln.equals("")) {
+ continue;
+ }
+ if (ln.startsWith("#")) {
+ continue;
+ }
+
+ final int sepIdx = ln.indexOf(' ');
+
+ /*
+ * Complain about improperly formatted lines.
+ */
+ if (sepIdx == -1) {
+ final String fmt = "Properties must be a name, a space, then the body.\n\tOffending line is '%s'";
+ final String msg = String.format(fmt, ln);
+
+ throw new NoSuchElementException(msg);
+ }
+
+ final String name = ln.substring(0, sepIdx).trim();
+ final String body = ln.substring(sepIdx).trim();
+
+ /*
+ * Complain about duplicates, if that is wanted.
+ */
+ if (!allowDuplicates && containsKey(name)) {
+ final String msg = String.format("Duplicate key '%s'", name);
+
+ throw new IllegalStateException(msg);
+ }
+
+ put(name, body);
+ }
+ }
+ }
+
+ /**
+ * Output the set of read properties.
+ */
+ public void outputProperties() {
+ System.out.println("Read properties:");
+
+ for (final Entry<String, String> entry : entrySet()) {
+ System.out.printf("\t'%s'\t'%s'\n", entry.getKey(), entry.getValue());
+ }
+
+ System.out.println();
+ }
+
+ @Override
+ public int size() {
+ return props.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return props.isEmpty();
+ }
+
+ @SuppressWarnings("unlikely-arg-type")
+ @Override
+ public boolean containsKey(final Object key) {
+ return props.containsKey(key);
+ }
+
+ @SuppressWarnings("unlikely-arg-type")
+ @Override
+ public boolean containsValue(final Object value) {
+ return props.containsValue(value);
+ }
+
+ @SuppressWarnings("unlikely-arg-type")
+ @Override
+ public String get(final Object key) {
+ return props.get(key);
+ }
+
+ @Override
+ public String put(final String key, final String value) {
+ return props.put(key, value);
+ }
+
+ @SuppressWarnings("unlikely-arg-type")
+ @Override
+ public String remove(final Object key) {
+ return props.remove(key);
+ }
+
+ @Override
+ public void putAll(final Map<? extends String, ? extends String> m) {
+ props.putAll(m);
+ }
+
+ @Override
+ public void clear() {
+ props.clear();
+ }
+
+ @Override
+ public Set<String> keySet() {
+ return props.keySet();
+ }
+
+ @Override
+ public Collection<String> values() {
+ return props.values();
+ }
+
+ @Override
+ public Set<java.util.Map.Entry<String, String>> entrySet() {
+ return props.entrySet();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ return props.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return props.hashCode();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/Block.java b/base/src/main/java/bjc/utils/ioutils/blocks/Block.java
new file mode 100644
index 0000000..15f3510
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/Block.java
@@ -0,0 +1,88 @@
+package bjc.utils.ioutils.blocks;
+
+/**
+ * Represents a block of text read in from a source.
+ *
+ * @author EVE
+ *
+ */
+public class Block {
+ /**
+ * The contents of this block.
+ */
+ public final String contents;
+
+ /**
+ * The line of the source this block started on.
+ */
+ public final int startLine;
+
+ /**
+ * The line of the source this block ended on.
+ */
+ public final int endLine;
+
+ /**
+ * The number of this block.
+ */
+ public final int blockNo;
+
+ /**
+ * Create a new block.
+ *
+ * @param blockNo
+ * The number of this block.
+ * @param contents
+ * The contents of this block.
+ * @param startLine
+ * The line this block started on.
+ * @param endLine
+ * The line this block ended.
+ */
+ public Block(final int blockNo, final String contents, final int startLine, final int endLine) {
+ this.contents = contents;
+ this.startLine = startLine;
+ this.endLine = endLine;
+ this.blockNo = blockNo;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + blockNo;
+ result = prime * result + (contents == null ? 0 : contents.hashCode());
+ result = prime * result + endLine;
+ result = prime * result + startLine;
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof Block)) return false;
+
+ final Block other = (Block) obj;
+
+ if (blockNo != other.blockNo) return false;
+
+ if (contents == null) {
+ if (other.contents != null) return false;
+ } else if (!contents.equals(other.contents)) return false;
+
+ if (endLine != other.endLine) return false;
+ if (startLine != other.startLine) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ String fmt = "Block #%d (from lines %d to %d), length: %d characters";
+
+ return String.format(fmt, blockNo, startLine, endLine, contents.length());
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java
new file mode 100644
index 0000000..3c695c6
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReader.java
@@ -0,0 +1,73 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.function.Consumer;
+
+/**
+ * A source of blocks of characters, marked with line numbers as to block
+ * start/block end.
+ *
+ * @author bjculkin
+ *
+ */
+public interface BlockReader extends AutoCloseable, Iterator<Block> {
+ /**
+ * Check if this reader has an available block.
+ *
+ * @return Whether or not another block is available.
+ */
+ boolean hasNextBlock();
+
+ /**
+ * Get the current block.
+ *
+ * @return The current block, or null if there is no current block.
+ */
+ Block getBlock();
+
+ /**
+ * Move to the next block.
+ *
+ * @return Whether or not the next block was successfully read.
+ */
+ boolean nextBlock();
+
+ /**
+ * Retrieve the number of blocks that have been read so far.
+ *
+ * @return The number of blocks read so far.
+ */
+ int getBlockCount();
+
+ @Override
+ void close() throws IOException;
+
+ /*
+ * Methods with default impls.
+ */
+
+ /**
+ * Execute an action for each remaining block.
+ *
+ * @param action
+ * The action to execute for each block
+ */
+ default void forEachBlock(final Consumer<Block> action) {
+ while (hasNext()) {
+ action.accept(next());
+ }
+ }
+
+ @Override
+ default boolean hasNext() {
+ return hasNextBlock();
+ }
+
+ @Override
+ default Block next() {
+ nextBlock();
+
+ return getBlock();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java
new file mode 100644
index 0000000..8bbb89c
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/BlockReaders.java
@@ -0,0 +1,81 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.Reader;
+
+/**
+ * Utility methods for constructing instances of {@link BlockReader}
+ *
+ * @author bjculkin
+ *
+ */
+public class BlockReaders {
+ /**
+ * Create a new simple block reader that works off a regex.
+ *
+ * @param blockDelim
+ * The regex that separates blocks.
+ *
+ * @param source
+ * The reader to get blocks from.
+ *
+ * @return A configured simple reader.
+ */
+ public static SimpleBlockReader simple(final String blockDelim, final Reader source) {
+ return new SimpleBlockReader(blockDelim, source);
+ }
+
+ /**
+ * Create a new pushback block reader.
+ *
+ * @param src
+ * The block reader to read blocks from.
+ *
+ * @return A configured pushback reader.
+ */
+ public static PushbackBlockReader pushback(final BlockReader src) {
+ return new PushbackBlockReader(src);
+ }
+
+ /**
+ * Create a new triggered block reader.
+ *
+ * @param source
+ * The block reader to read blocks from.
+ *
+ * @param action
+ * The action to execute before reading a block.
+ *
+ * @return A configured triggered block reader.
+ */
+ public static BlockReader trigger(final BlockReader source, final Runnable action) {
+ return new TriggeredBlockReader(source, action);
+ }
+
+ /**
+ * Create a new layered block reader.
+ *
+ * @param primary
+ * The first source to read blocks from.
+ *
+ * @param secondary
+ * The second source to read blocks from.
+ *
+ * @return A configured layered block reader.
+ */
+ public static BlockReader layered(final BlockReader primary, final BlockReader secondary) {
+ return new LayeredBlockReader(primary, secondary);
+ }
+
+ /**
+ * Create a new serial block reader.
+ *
+ * @param readers
+ * The readers to pull from, in the order to pull from
+ * them.
+ *
+ * @return A configured serial block reader.
+ */
+ public static BlockReader serial(final BlockReader... readers) {
+ return new SerialBlockReader(readers);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java
new file mode 100644
index 0000000..b1e82d7
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/BoundBlockReader.java
@@ -0,0 +1,61 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+import java.util.function.BooleanSupplier;
+import java.util.function.Supplier;
+
+public class BoundBlockReader implements BlockReader {
+ @FunctionalInterface
+ public interface Closer {
+ public void close() throws IOException;
+ }
+
+ private BooleanSupplier checker;
+ private Supplier<Block> getter;
+ private Closer closer;
+
+ private Block current;
+
+ private int blockNo;
+
+ public BoundBlockReader(BooleanSupplier blockChecker, Supplier<Block> blockGetter, Closer blockCloser) {
+ checker = blockChecker;
+ getter = blockGetter;
+ closer = blockCloser;
+
+ blockNo = 0;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return checker.getAsBoolean();
+ }
+
+ @Override
+ public Block getBlock() {
+ return current;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ if(checker.getAsBoolean()) {
+ current = getter.get();
+ blockNo += 1;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ closer.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java
new file mode 100644
index 0000000..0b43f7a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/FilteredBlockReader.java
@@ -0,0 +1,97 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public class FilteredBlockReader implements BlockReader {
+ /*
+ * The source of blocks.
+ */
+ private BlockReader source;
+
+ /*
+ * The current and next block.
+ *
+ * Both have already been checked for the predicate.
+ */
+ private Block current;
+ private Block pending;
+
+ /*
+ * Number of blocks that passed the predicate.
+ */
+ private int blockNo;
+
+ /*
+ * The predicate blocks must pass.
+ */
+ private Predicate<Block> pred;
+
+ /*
+ * The action to call on failure, if there is one.
+ */
+ private Consumer<Block> failAction;
+
+ public FilteredBlockReader(BlockReader src, Predicate<Block> predic) {
+ this(src, predic, null);
+ }
+
+ public FilteredBlockReader(BlockReader src, Predicate<Block> predic, Consumer<Block> failAct) {
+ source = src;
+ pred = predic;
+ failAction = failAct;
+
+ blockNo = 0;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ if(pending != null) return true;
+
+ while(source.hasNextBlock()) {
+ /*
+ * Only say we have a next block if the next block would
+ * pass the predicate.
+ */
+ pending = source.next();
+
+ if(pred.test(pending)) {
+ blockNo += 1;
+ return true;
+ } else {
+ failAction.accept(pending);
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public Block getBlock() {
+ return current;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ if(pending != null || hasNextBlock()) {
+ current = pending;
+ pending = null;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java
new file mode 100644
index 0000000..f4d8439
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/FlatMappedBlockReader.java
@@ -0,0 +1,86 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * A block reader that supports applying a flatmap operation to blocks.
+ *
+ * The use-case in mind for this was tokenizing blocks.
+ *
+ * @author Benjamin Culkin
+ */
+public class FlatMappedBlockReader implements BlockReader {
+ /*
+ * The source reader.
+ */
+ private BlockReader reader;
+
+ /*
+ * The current block, and any blocks pending from the last source block.
+ */
+ private Iterator<Block> pending;
+ private Block current;
+
+ /*
+ * The operator to open blocks with.
+ */
+ private Function<Block, List<Block>> transform;
+
+ /*
+ * The current block number.
+ */
+ private int blockNo;
+
+ public FlatMappedBlockReader(BlockReader source, Function<Block, List<Block>> trans) {
+ reader = source;
+ transform = trans;
+
+ blockNo = 0;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return pending.hasNext() || reader.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ return current;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ /*
+ * Attempt to get a new pending list if the one we have isn't
+ * valid.
+ */
+ while(pending == null || !pending.hasNext()) {
+ if(!reader.hasNext()) return false;
+
+ pending = transform.apply(reader.next()).iterator();
+ }
+
+ /*
+ * Advance the iterator.
+ */
+ current = pending.next();
+ blockNo += 1;
+
+ return true;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java
new file mode 100644
index 0000000..967a1f2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/LayeredBlockReader.java
@@ -0,0 +1,81 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+/**
+ * A block reader that supports draining all the blocks from one reading before
+ * swapping to another.
+ *
+ * This is more a 'prioritize blocks from one over the other', than a 'read all
+ * the blocks from one, then all the blocks from the other'. If you need that,
+ * look at {@link SerialBlockReader}.
+ *
+ * @author bjculkin
+ *
+ */
+public class LayeredBlockReader implements BlockReader {
+ /*
+ * The readers to drain from.
+ */
+ private final BlockReader first;
+ private final BlockReader second;
+
+ /*
+ * The current block number.
+ */
+ private int blockNo;
+
+ /**
+ * Create a new layered block reader.
+ *
+ * @param primary
+ * The first source to read blocks from.
+ *
+ * @param secondary
+ * The second source to read blocks from.
+ */
+ public LayeredBlockReader(final BlockReader primary, final BlockReader secondary) {
+ first = primary;
+ second = secondary;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return first.hasNextBlock() || second.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ final Block firstBlock = first.getBlock();
+
+ /*
+ * Only drain a block from the second reader if none are
+ * available in the first reader.
+ */
+ return firstBlock == null ? second.getBlock() : firstBlock;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ final boolean gotFirst = first.nextBlock();
+ final boolean succ = gotFirst ? gotFirst : second.nextBlock();
+
+ if (succ) {
+ blockNo += 1;
+ }
+
+ return succ;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ second.close();
+
+ first.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java
new file mode 100644
index 0000000..12fa848
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/MappedBlockReader.java
@@ -0,0 +1,54 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+import java.util.function.UnaryOperator;
+
+public class MappedBlockReader implements BlockReader {
+ private BlockReader reader;
+
+ private Block current;
+
+ private UnaryOperator<Block> transform;
+
+ private int blockNo;
+
+ public MappedBlockReader(BlockReader source, UnaryOperator<Block> trans) {
+ reader = source;
+ transform = trans;
+
+ blockNo = 0;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return reader.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ return current;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ if(hasNextBlock()) {
+ current = transform.apply(reader.next());
+ blockNo += 1;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java
new file mode 100644
index 0000000..0cc9dea
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/PushbackBlockReader.java
@@ -0,0 +1,106 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+import java.util.Deque;
+import java.util.LinkedList;
+
+/**
+ * A block reader that supports pushing blocks onto the input queue so that they
+ * are provided before blocks read from an input source.
+ *
+ * @author bjculkin
+ *
+ */
+public class PushbackBlockReader implements BlockReader {
+ private final BlockReader source;
+
+ /*
+ * The queue of pushed-back blocks.
+ */
+ private final Deque<Block> waiting;
+
+ private Block curBlock;
+
+ private int blockNo;
+
+ /**
+ * Create a new pushback block reader.
+ *
+ * @param src
+ * The block reader to use when no blocks are queued.
+ */
+ public PushbackBlockReader(final BlockReader src) {
+ source = src;
+
+ waiting = new LinkedList<>();
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return !waiting.isEmpty() || source.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ return curBlock;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ /*
+ * Drain pushed-back blocks first.
+ */
+ if (!waiting.isEmpty()) {
+ curBlock = waiting.pop();
+
+ blockNo += 1;
+
+ return true;
+ } else {
+ final boolean succ = source.nextBlock();
+ curBlock = source.getBlock();
+
+ if (succ) {
+ blockNo += 1;
+ }
+
+ return succ;
+ }
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+
+ /**
+ * Insert a block at the back of the queue of pending blocks.
+ *
+ * @param blk
+ * The block to put at the back.
+ */
+ public void addBlock(final Block blk) {
+ waiting.add(blk);
+ }
+
+ /**
+ * Insert a block at the front of the queue of pending blocks.
+ *
+ * @param blk
+ * The block to put at the front.
+ */
+ public void pushBlock(final Block blk) {
+ waiting.push(blk);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PushbackBlockReader [waiting=%s, curBlock=%s, blockNo=%s]", waiting, curBlock,
+ blockNo);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java
new file mode 100644
index 0000000..c229da1
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/SerialBlockReader.java
@@ -0,0 +1,102 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+import java.util.Deque;
+
+/**
+ * Provides a means of concatenating two block readers.
+ *
+ * @author bjculkin
+ *
+ */
+public class SerialBlockReader implements BlockReader {
+ private Deque<BlockReader> readerQueue;
+
+ private int blockNo;
+
+ /**
+ * Create a new serial block reader.
+ *
+ * @param readers
+ * The readers to pull from, in the order to pull from
+ * them.
+ */
+ public SerialBlockReader(final BlockReader... readers) {
+ for (final BlockReader reader : readers) {
+ readerQueue.add(reader);
+ }
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ if (readerQueue.isEmpty()) return false;
+
+ /*
+ * Attempt to get a block from the first reader.
+ */
+ boolean hasBlock = readerQueue.peek().hasNextBlock();
+ boolean cont = hasBlock || readerQueue.isEmpty();
+
+ /*
+ * Close/dispose of readers until we get an open one.
+ */
+ while (!cont) {
+ try {
+ readerQueue.pop().close();
+ } catch (final IOException ioex) {
+ throw new IllegalStateException("Exception thrown by discarded reader", ioex);
+ }
+
+ hasBlock = readerQueue.peek().hasNextBlock();
+ cont = hasBlock || readerQueue.isEmpty();
+ }
+
+ return hasBlock;
+ }
+
+ @Override
+ public Block getBlock() {
+ if (readerQueue.isEmpty())
+ return null;
+ else return readerQueue.peek().getBlock();
+ }
+
+ @Override
+ public boolean nextBlock() {
+ if (readerQueue.isEmpty()) return false;
+
+ boolean gotBlock = readerQueue.peek().nextBlock();
+ boolean cont = gotBlock || readerQueue.isEmpty();
+
+ while (!cont) {
+ try {
+ readerQueue.pop().close();
+ } catch (final IOException ioex) {
+ throw new IllegalStateException("Exception thrown by discarded reader", ioex);
+ }
+
+ gotBlock = readerQueue.peek().nextBlock();
+ cont = gotBlock || readerQueue.isEmpty();
+ }
+
+ if (cont) {
+ blockNo += 1;
+ }
+
+ return cont;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ while (!readerQueue.isEmpty()) {
+ final BlockReader reader = readerQueue.pop();
+
+ reader.close();
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java
new file mode 100644
index 0000000..734bde8
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/SimpleBlockReader.java
@@ -0,0 +1,115 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+import java.util.regex.Pattern;
+
+import bjc.utils.funcutils.StringUtils;
+/**
+ * Simple implementation of {@link BlockReader}
+ *
+ * NOTE: The EOF marker is always treated as a delimiter. You are expected to
+ * handle blocks that may be shorter than you expect.
+ *
+ * @author EVE
+ *
+ */
+public class SimpleBlockReader implements BlockReader {
+ /*
+ * I/O source for blocks.
+ */
+ private final Scanner blockReader;
+
+ /*
+ * The current block.
+ */
+ private Block currBlock;
+
+ /*
+ * Info about the current block.
+ */
+ private int blockNo;
+ private int lineNo;
+
+ /**
+ * Create a new block reader.
+ *
+ * @param blockDelim
+ * The pattern that separates blocks. Note that the end
+ * of file is always considered to end a block.
+ *
+ * @param source
+ * The source to read blocks from.
+ */
+ public SimpleBlockReader(final String blockDelim, final Reader source) {
+ blockReader = new Scanner(source);
+
+ final String pattern = String.format("(?:%s)|\\Z", blockDelim);
+ final Pattern pt = Pattern.compile(pattern, Pattern.MULTILINE);
+
+ blockReader.useDelimiter(pt);
+
+ lineNo = 1;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ return blockReader.hasNext();
+ }
+
+ @Override
+ public Block getBlock() {
+ return currBlock;
+ }
+
+ @Override
+ public boolean nextBlock() {
+ try {
+ /*
+ * Read in a new block, and keep the line numbers sane.
+ */
+ final int blockStartLine = lineNo;
+ final String blockContents = blockReader.next();
+ final int blockEndLine = lineNo + StringUtils.countMatches(blockContents, "\\R");
+
+ lineNo = blockEndLine;
+ blockNo += 1;
+
+ currBlock = new Block(blockNo, blockContents, blockStartLine, blockEndLine);
+
+ return true;
+ } catch (final NoSuchElementException nseex) {
+ currBlock = null;
+
+ return false;
+ }
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ blockReader.close();
+ }
+
+ /**
+ * Set the delimiter used to separate blocks.
+ *
+ * @param delim
+ * The delimiter used to separate blocks.
+ */
+ public void setDelimiter(final String delim) {
+ blockReader.useDelimiter(delim);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SimpleBlockReader [currBlock=%s, blockNo=%s]", currBlock, blockNo);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java
new file mode 100644
index 0000000..8f39b8f
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/ToggledBlockReader.java
@@ -0,0 +1,63 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+import bjc.utils.data.BooleanToggle;
+
+public class ToggledBlockReader implements BlockReader {
+ private BlockReader leftSource;
+ private BlockReader rightSource;
+
+ /*
+ * We choose the left source when this is true.
+ */
+ private BooleanToggle leftToggle;
+
+ private int blockNo;
+
+ public ToggledBlockReader(BlockReader left, BlockReader right) {
+ leftSource = left;
+ rightSource = right;
+
+ blockNo = 0;
+
+ leftToggle = new BooleanToggle();
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ if(leftToggle.peek()) return leftSource.hasNextBlock();
+ else return rightSource.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ if(leftToggle.peek()) return leftSource.getBlock();
+ else return rightSource.getBlock();
+ }
+
+ @Override
+ public boolean nextBlock() {
+ boolean succ;
+
+ if(leftToggle.get()) {
+ succ = leftSource.nextBlock();
+ } else {
+ succ = rightSource.nextBlock();
+ }
+
+ if(succ) blockNo += 1;
+ return succ;
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ leftSource.close();
+ rightSource.close();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java b/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java
new file mode 100644
index 0000000..3a1e393
--- /dev/null
+++ b/base/src/main/java/bjc/utils/ioutils/blocks/TriggeredBlockReader.java
@@ -0,0 +1,70 @@
+package bjc.utils.ioutils.blocks;
+
+import java.io.IOException;
+
+/**
+ * A block reader that fires an action before a block is actually read.
+ *
+ * @author bjculkin
+ *
+ */
+public class TriggeredBlockReader implements BlockReader {
+ private final BlockReader source;
+
+ private int blockNo;
+
+ /*
+ * The action to fire.
+ */
+ private final Runnable action;
+
+ /**
+ * Create a new triggered reader with the specified source/action.
+ *
+ * @param source
+ * The block reader to read blocks from.
+ *
+ * @param action
+ * The action to execute before reading a block.
+ */
+ public TriggeredBlockReader(final BlockReader source, final Runnable action) {
+ this.source = source;
+ this.action = action;
+
+ blockNo = 0;
+ }
+
+ @Override
+ public boolean hasNextBlock() {
+ action.run();
+
+ return source.hasNextBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ return source.getBlock();
+ }
+
+ @Override
+ public boolean nextBlock() {
+ blockNo += 1;
+
+ return source.nextBlock();
+ }
+
+ @Override
+ public int getBlockCount() {
+ return blockNo;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TriggeredBlockReader [source=%s]", source);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java b/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java
new file mode 100644
index 0000000..a885808
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/DoubleMatcher.java
@@ -0,0 +1,46 @@
+package bjc.utils.parserutils;
+
+import static bjc.utils.PropertyDB.applyFormat;
+import static bjc.utils.PropertyDB.getRegex;
+
+import java.util.regex.Pattern;
+
+/*
+ * Checks if a string would pass Double.parseDouble.
+ *
+ * Uses a regex from the javadoc for Double.valueOf()
+ */
+class DoubleMatcher {
+ /*
+ * Unit pieces.
+ */
+ private static final String rDecDigits = getRegex("fpDigits");
+ private static final String rHexDigits = getRegex("fpHexDigits");
+ private static final String rExponent = applyFormat("fpExponent", getRegex("fpExponent"), rDecDigits);
+
+ /*
+ * Decimal floating point numbers.
+ */
+ private static final String rSimpleDec = applyFormat("fpDecimalDecimal", rDecDigits, rExponent);
+ private static final String rSimpleIntDec = applyFormat("fpDecimalInteger", rDecDigits, rExponent);
+
+ /*
+ * Hex floating point numbers.
+ */
+ private static final String rHexInt = applyFormat("fpHexInteger", rHexDigits);
+ private static final String rHexDec = applyFormat("fpHexDecimal", rHexDigits);
+ private static final String rHexLead = applyFormat("fpHexLeader", rHexInt, rHexDec);
+ private static final String rHexString = applyFormat("fpHexString", rHexLead, rDecDigits);
+
+ /*
+ * Floating point components.
+ */
+ private static final String rFPLeader = getRegex("fpLeader");
+ private static final String rFPNum = applyFormat("fpNumber", rSimpleIntDec, rSimpleDec, rHexString);
+
+ /*
+ * Full double.
+ */
+ private static final String rDouble = applyFormat("fpDouble", rFPLeader, rFPNum);
+ public static final Pattern doubleLiteral = Pattern.compile("\\A" + rDouble + "\\Z");
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/IPrecedent.java b/base/src/main/java/bjc/utils/parserutils/IPrecedent.java
new file mode 100644
index 0000000..aa366cf
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/IPrecedent.java
@@ -0,0 +1,28 @@
+package bjc.utils.parserutils;
+
+/**
+ * Represents something that has a set precedence
+ *
+ * @author ben
+ *
+ */
+@FunctionalInterface
+public interface IPrecedent {
+ /**
+ * Create a new object with set precedence
+ *
+ * @param precedence
+ * The precedence of the object to handle
+ * @return A new object with set precedence
+ */
+ public static IPrecedent newSimplePrecedent(final int precedence) {
+ return () -> precedence;
+ }
+
+ /**
+ * Get the precedence of the attached object
+ *
+ * @return The precedence of the attached object
+ */
+ public int getPrecedence();
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/ParserException.java b/base/src/main/java/bjc/utils/parserutils/ParserException.java
new file mode 100644
index 0000000..ae33aba
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/ParserException.java
@@ -0,0 +1,36 @@
+package bjc.utils.parserutils;
+
+/**
+ * General superclass for exceptions thrown during parsing.
+ *
+ * @author EVE
+ *
+ */
+public class ParserException extends Exception {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 631298568113373233L;
+
+ /**
+ * Create a new exception with the provided message.
+ *
+ * @param msg
+ * The message for the exception.
+ */
+ public ParserException(final String msg) {
+ super(msg);
+ }
+
+ /**
+ * Create a new exception with the provided message and cause.
+ *
+ * @param msg
+ * The message for the exception.
+ * @param cause
+ * The cause of the exception.
+ */
+ public ParserException(final String msg, final Exception cause) {
+ super(msg, cause);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java b/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java
new file mode 100644
index 0000000..a1b5feb
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/ShuntingYard.java
@@ -0,0 +1,274 @@
+package bjc.utils.parserutils;
+
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.FunctionalMap;
+import bjc.utils.funcdata.IList;
+import bjc.utils.funcdata.IMap;
+import bjc.utils.funcutils.StringUtils;
+
+/**
+ * Utility to run the shunting yard algorithm on a bunch of tokens.
+ *
+ * @author ben
+ *
+ * @param <TokenType>
+ * The type of tokens being shunted.
+ */
+public class ShuntingYard<TokenType> {
+ /**
+ * A enum representing the fundamental operator types.
+ *
+ * @author ben
+ *
+ */
+ public static enum Operator implements IPrecedent {
+ /**
+ * Represents addition.
+ */
+ ADD(1),
+ /**
+ * Represents subtraction.
+ */
+ SUBTRACT(2),
+
+ /**
+ * Represents multiplication.
+ */
+ MULTIPLY(3),
+ /**
+ * Represents division.
+ */
+ DIVIDE(4);
+
+ private final int precedence;
+
+ private Operator(final int prec) {
+ precedence = prec;
+ }
+
+ @Override
+ public int getPrecedence() {
+ return precedence;
+ }
+ }
+
+ /*
+ * Function that shunts tokens.
+ */
+ private final class TokenShunter implements Consumer<String> {
+ private final IList<TokenType> output;
+ private final Deque<String> stack;
+ private final Function<String, TokenType> transformer;
+
+ public TokenShunter(final IList<TokenType> outpt, final Deque<String> stack,
+ final Function<String, TokenType> transformer) {
+ this.output = outpt;
+ this.stack = stack;
+ this.transformer = transformer;
+ }
+
+ @Override
+ public void accept(final String token) {
+ /*
+ * Handle operators
+ */
+ if (operators.containsKey(token)) {
+ /*
+ * Pop operators while there isn't a higher precedence one
+ */
+ while (!stack.isEmpty() && isHigherPrec(token, stack.peek())) {
+ output.add(transformer.apply(stack.pop()));
+ }
+
+ /*
+ * Put this operator onto the stack
+ */
+ stack.push(token);
+ } else if (StringUtils.containsOnly(token, "\\(")) {
+ /*
+ * Handle groups of parenthesis for multiple nesting levels
+ */
+ stack.push(token);
+ } else if (StringUtils.containsOnly(token, "\\)")) {
+ /*
+ * Handle groups of parenthesis for multiple nesting levels
+ */
+ final String swappedToken = token.replace(')', '(');
+
+ /*
+ * Remove tokens up to a matching parenthesis
+ */
+ while (!stack.peek().equals(swappedToken)) {
+ output.add(transformer.apply(stack.pop()));
+ }
+
+ /*
+ * Remove the parenthesis
+ */
+ stack.pop();
+ } else {
+ /*
+ * Just add the transformed token
+ */
+ output.add(transformer.apply(token));
+ }
+ }
+ }
+
+ /*
+ * Holds all the shuntable operations.
+ */
+ private IMap<String, IPrecedent> operators;
+
+ /**
+ * Create a new shunting yard with a default set of operators.
+ *
+ * @param configureBasics
+ * Whether or not basic math operators should be
+ * provided.
+ */
+ public ShuntingYard(final boolean configureBasics) {
+ operators = new FunctionalMap<>();
+
+ /*
+ * Add basic operators if we're configured to do so
+ */
+ if (configureBasics) {
+ operators.put("+", Operator.ADD);
+ operators.put("-", Operator.SUBTRACT);
+ operators.put("*", Operator.MULTIPLY);
+ operators.put("/", Operator.DIVIDE);
+ }
+ }
+
+ /**
+ * Add an operator to the list of shuntable operators.
+ *
+ * @param operator
+ * The token representing the operator.
+ *
+ * @param precedence
+ * The precedence of the operator to add.
+ */
+ public void addOp(final String operator, final int precedence) {
+ /*
+ * Create the precedence marker
+ */
+ final IPrecedent prec = IPrecedent.newSimplePrecedent(precedence);
+
+ this.addOp(operator, prec);
+ }
+
+ /**
+ * Add an operator to the list of shuntable operators.
+ *
+ * @param operator
+ * The token representing the operator.
+ *
+ * @param precedence
+ * The precedence of the operator.
+ */
+ public void addOp(final String operator, final IPrecedent precedence) {
+ /*
+ * Complain about trying to add an incorrect operator
+ */
+ if (operator == null)
+ throw new NullPointerException("Operator must not be null");
+ else if (precedence == null) throw new NullPointerException("Precedence must not be null");
+
+ /*
+ * Add the operator to the ones we handle
+ */
+ operators.put(operator, precedence);
+ }
+
+ private boolean isHigherPrec(final String left, final String right) {
+ /*
+ * Check if the right operator exists
+ */
+ final boolean exists = operators.containsKey(right);
+
+ /*
+ * If it doesn't, the left is higher precedence.
+ */
+ if (!exists) return false;
+
+ /*
+ * Get the precedence of operators
+ */
+ final int rightPrecedence = operators.get(right).getPrecedence();
+ final int leftPrecedence = operators.get(left).getPrecedence();
+
+ /*
+ * Evaluate what we were asked
+ */
+ return rightPrecedence >= leftPrecedence;
+ }
+
+ /**
+ * Transform a string of tokens from infix notation to postfix.
+ *
+ * @param input
+ * The string to transform.
+ *
+ * @param transformer
+ * The function to use to transform strings to tokens.
+ *
+ * @return A list of tokens in postfix notation.
+ */
+ public IList<TokenType> postfix(final IList<String> input, final Function<String, TokenType> transformer) {
+ /*
+ * Check our input
+ */
+ if (input == null)
+ throw new NullPointerException("Input must not be null");
+ else if (transformer == null) throw new NullPointerException("Transformer must not be null");
+
+ /*
+ * Here's what we're handing back
+ */
+ final IList<TokenType> output = new FunctionalList<>();
+
+ /*
+ * The stack to put operators on
+ */
+ final Deque<String> stack = new LinkedList<>();
+
+ /*
+ * Shunt the tokens
+ */
+ input.forEach(new TokenShunter(output, stack, transformer));
+
+ /*
+ * Transform any resulting tokens
+ */
+ stack.forEach(token -> {
+ output.add(transformer.apply(token));
+ });
+
+ return output;
+ }
+
+ /**
+ * Remove an operator from the list of shuntable operators.
+ *
+ * @param operator
+ * The token representing the operator. If null, remove
+ * all operators.
+ */
+ public void removeOp(final String operator) {
+ /*
+ * Check if we want to remove all operators
+ */
+ if (operator == null) {
+ operators = new FunctionalMap<>();
+ } else {
+ operators.remove(operator);
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/StringDescaper.java b/base/src/main/java/bjc/utils/parserutils/StringDescaper.java
new file mode 100644
index 0000000..096656a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/StringDescaper.java
@@ -0,0 +1,242 @@
+package bjc.utils.parserutils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import static java.util.Map.Entry;
+
+import static bjc.utils.PropertyDB.applyFormat;
+import static bjc.utils.PropertyDB.getCompiledRegex;
+import static bjc.utils.PropertyDB.getRegex;
+
+public class StringDescaper {
+ private Logger LOGGER = Logger.getLogger(StringDescaper.class.getName());
+
+ /*
+ * Patterns and pattern parts.
+ */
+ private static String rPossibleEscapeString = getRegex("possibleStringEscape");
+ private static Pattern possibleEscapePatt = Pattern.compile(rPossibleEscapeString);
+
+ private static String rShortEscape = getRegex("shortFormStringEscape");
+ private static String rOctalEscape = getRegex("octalStringEscape");
+ private static String rUnicodeEscape = getRegex("unicodeStringEscape");
+
+ private String rEscapeString;
+ private Pattern escapePatt;
+
+ private static String rDoubleQuoteString = applyFormat("doubleQuotes", getRegex("nonStringEscape"), rPossibleEscapeString);
+ private static Pattern doubleQuotePatt = Pattern.compile(rDoubleQuoteString);
+
+ private static Pattern quotePatt = getCompiledRegex("unescapedQuote");
+
+ private Map<String, String> literalEscapes;
+ private Map<Pattern, UnaryOperator<String>> specialEscapes;
+
+ public StringDescaper() {
+ literalEscapes = new HashMap<>();
+ specialEscapes = new HashMap<>();
+
+ rEscapeString = String.format("\\\\(%1$s|%2$s|%3$s)");
+ escapePatt = Pattern.compile(rEscapeString);
+ }
+
+ public void addLiteralEscape(String escape, String val) {
+ if(literalEscapes.containsKey(escape)) {
+ LOGGER.warning(String.format("Shadowing literal escape '%s'\n", escape));
+ }
+
+ literalEscapes.put(escape, val);
+ }
+
+ public void addSpecialEscape(String escape, UnaryOperator<String> val) {
+ if(specialEscapes.containsKey(escape)) {
+ LOGGER.warning(String.format("Shadowing special escape '%s'\n", escape));
+ }
+
+ /*
+ * Make sure this special escape is a valid regex.
+ */
+
+ Pattern patt = null;
+ try {
+ patt = Pattern.compile(escape);
+ } catch (PatternSyntaxException psex) {
+ String msg = String.format("Invalid special escape '%s'", escape);
+
+ IllegalArgumentException iaex = new IllegalArgumentException(msg);
+ iaex.initCause(psex);
+
+ throw psex;
+ }
+
+ specialEscapes.put(patt, val);
+ }
+
+ public void compileEscapes() {
+ StringBuilder work = new StringBuilder();
+
+ for(String litEscape : literalEscapes.keySet()) {
+ work.append("|(?:");
+ work.append(Pattern.quote(litEscape));
+ work.append(")");
+ }
+
+ for(Pattern specEscape : specialEscapes.keySet()) {
+ work.append("|(?:");
+ work.append(specEscape.toString());
+ work.append(")");
+ }
+
+ /*
+ * Convert user-defined escapes to a regex for matching.
+ * We don't need a bar before %4 because the string has it.
+ */
+ rEscapeString = String.format("\\(%1$s|%2$s|%3$s%4$s)", rShortEscape, rOctalEscape, rUnicodeEscape, work.toString());
+ escapePatt = Pattern.compile(rEscapeString);
+ }
+
+ /**
+ * Replace escape characters with their actual equivalents.
+ *
+ * @param inp
+ * The string to replace escape sequences in.
+ *
+ * @return The string with escape sequences replaced by their equivalent
+ * characters.
+ */
+ public String descapeString(final String inp) {
+ if (inp == null) {
+ throw new NullPointerException("Input to descapeString must not be null");
+ }
+
+ /*
+ * Prepare the buffer and escape finder.
+ */
+ final StringBuffer work = new StringBuffer();
+ final Matcher possibleEscapeFinder = possibleEscapePatt.matcher(inp);
+ final Matcher escapeFinder = escapePatt.matcher(inp);
+
+ while (possibleEscapeFinder.find()) {
+ if (!escapeFinder.find()) {
+ /*
+ * Found a possible escape that isn't actually an
+ * escape.
+ */
+ final String msg = String.format("Illegal escape sequence '%s' at position %d of string '%s'",
+ possibleEscapeFinder.group(), possibleEscapeFinder.start(), inp);
+ throw new IllegalArgumentException(msg);
+ }
+
+ final String escapeSeq = escapeFinder.group();
+
+ /*
+ * Convert the escape to a string.
+ */
+ String escapeRep = "";
+ switch (escapeSeq) {
+ case "\\b":
+ escapeRep = "\b";
+ break;
+ case "\\t":
+ escapeRep = "\t";
+ break;
+ case "\\n":
+ escapeRep = "\n";
+ break;
+ case "\\f":
+ escapeRep = "\f";
+ break;
+ case "\\r":
+ escapeRep = "\r";
+ break;
+ case "\\\"":
+ escapeRep = "\"";
+ break;
+ case "\\'":
+ escapeRep = "'";
+ break;
+ case "\\\\":
+ /*
+ * Skip past the second slash.
+ */
+ possibleEscapeFinder.find();
+ escapeRep = "\\";
+ break;
+ default:
+ if (escapeSeq.startsWith("u")) {
+ escapeRep = handleUnicodeEscape(escapeSeq.substring(1));
+ } else if(escapeSeq.startsWith("O")) {
+ escapeRep = handleOctalEscape(escapeSeq.substring(1));
+ } else if(literalEscapes.containsKey(escapeSeq)) {
+ escapeRep = literalEscapes.get(escapeSeq);
+ } else {
+ for(Entry<Pattern, UnaryOperator<String>> ent : specialEscapes.entrySet()) {
+ Pattern pat = ent.getKey();
+
+ Matcher mat = pat.matcher(escapeSeq);
+ if(mat.matches()) {
+ escapeRep = ent.getValue().apply(escapeSeq);
+ break;
+ }
+ }
+ }
+ }
+
+ escapeFinder.appendReplacement(work, escapeRep);
+ }
+
+ escapeFinder.appendTail(work);
+
+ return work.toString();
+ }
+
+ /*
+ * Handle a unicode codepoint.
+ */
+ private static String handleUnicodeEscape(final String seq) {
+ try {
+ final int codepoint = Integer.parseInt(seq, 16);
+
+ return new String(Character.toChars(codepoint));
+ } catch (final IllegalArgumentException iaex) {
+ final String msg = String.format("'%s' is not a valid Unicode escape sequence'", seq);
+
+ final IllegalArgumentException reiaex = new IllegalArgumentException(msg);
+
+ reiaex.initCause(iaex);
+
+ throw reiaex;
+ }
+ }
+
+ /*
+ * Handle a octal codepoint.
+ */
+ private static String handleOctalEscape(final String seq) {
+ try {
+ final int codepoint = Integer.parseInt(seq, 8);
+
+ if (codepoint > 255) {
+ final String msg = String.format("'%d' is outside the range of octal escapes', codepoint");
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ return new String(Character.toChars(codepoint));
+ } catch (final IllegalArgumentException iaex) {
+ final String msg = String.format("'%s' is not a valid octal escape sequence'", seq);
+
+ final IllegalArgumentException reiaex = new IllegalArgumentException(msg);
+
+ reiaex.initCause(iaex);
+
+ throw reiaex;
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java b/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java
new file mode 100644
index 0000000..30ccc5a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/TokenTransformer.java
@@ -0,0 +1,131 @@
+package bjc.utils.parserutils;
+
+import java.util.Deque;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.ITree;
+import bjc.utils.data.Pair;
+import bjc.utils.data.Tree;
+import bjc.utils.parserutils.TreeConstructor.ConstructorState;
+import bjc.utils.parserutils.TreeConstructor.QueueFlattener;
+
+/*
+ * Handle creating ASTs from tokens.
+ */
+final class TokenTransformer<TokenType> implements Consumer<TokenType> {
+ /*
+ * Handle operators
+ */
+ private final class OperatorHandler implements UnaryOperator<ConstructorState<TokenType>> {
+ private final TokenType element;
+
+ public OperatorHandler(final TokenType element) {
+ this.element = element;
+ }
+
+ @Override
+ public ConstructorState<TokenType> apply(final ConstructorState<TokenType> pair) {
+ /*
+ * Replace the current AST with the result of handling an operator
+ */
+ return new ConstructorState<>(pair.bindLeft(queuedASTs -> {
+ return handleOperator(queuedASTs);
+ }));
+ }
+
+ private ConstructorState<TokenType> handleOperator(final Deque<ITree<TokenType>> queuedASTs) {
+ /*
+ * The AST we're going to hand back
+ */
+ ITree<TokenType> newAST;
+
+ /*
+ * Handle special operators
+ */
+ if (isSpecialOperator.test(element)) {
+ newAST = handleSpecialOperator.apply(element).apply(queuedASTs);
+ } else {
+ /*
+ * Error if we don't have enough for a binary operator
+ */
+ if (queuedASTs.size() < 2) {
+ final String msg = String.format(
+ "Attempted to parse binary operator without enough operands\n\tProblem operator is: %s\n\tPossible operand is: %s",
+ element.toString(), queuedASTs.peek().toString());
+
+ throw new IllegalStateException(msg);
+ }
+
+ /*
+ * Grab the two operands
+ */
+ final ITree<TokenType> right = queuedASTs.pop();
+ final ITree<TokenType> left = queuedASTs.pop();
+
+ /*
+ * Create a new AST
+ */
+ newAST = new Tree<>(element, left, right);
+ }
+
+ /*
+ * Stick it onto the stack
+ */
+ queuedASTs.push(newAST);
+
+ /*
+ * Hand back the state
+ */
+ return new ConstructorState<>(queuedASTs, newAST);
+ }
+ }
+
+ private final IHolder<ConstructorState<TokenType>> initialState;
+
+ private final Predicate<TokenType> operatorPredicate;
+
+ private final Predicate<TokenType> isSpecialOperator;
+ private final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator;
+
+ /*
+ * Create a new transformer
+ */
+ public TokenTransformer(final IHolder<ConstructorState<TokenType>> initialState,
+ final Predicate<TokenType> operatorPredicate, final Predicate<TokenType> isSpecialOperator,
+ final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator) {
+ this.initialState = initialState;
+ this.operatorPredicate = operatorPredicate;
+ this.isSpecialOperator = isSpecialOperator;
+ this.handleSpecialOperator = handleSpecialOperator;
+ }
+
+ @Override
+ public void accept(final TokenType element) {
+ /*
+ * Handle operators
+ */
+ if (operatorPredicate.test(element)) {
+ initialState.transform(new OperatorHandler(element));
+ } else {
+ final ITree<TokenType> newAST = new Tree<>(element);
+
+ /*
+ * Insert the new tree into the AST
+ */
+ initialState.transform(pair -> {
+ /*
+ * Transform the pair, ignoring the current AST in favor of the one consisting of the current element
+ */
+ return new ConstructorState<>(pair.bindLeft(queue -> {
+ queue.push(newAST);
+
+ return new Pair<>(queue, newAST);
+ }));
+ });
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/TokenUtils.java b/base/src/main/java/bjc/utils/parserutils/TokenUtils.java
new file mode 100644
index 0000000..67c1e5a
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/TokenUtils.java
@@ -0,0 +1,303 @@
+package bjc.utils.parserutils;
+
+import static bjc.utils.PropertyDB.applyFormat;
+import static bjc.utils.PropertyDB.getCompiledRegex;
+import static bjc.utils.PropertyDB.getRegex;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+import bjc.utils.parserutils.splitter.TokenSplitter;
+
+/**
+ * Utilities useful for operating on PL tokens.
+ *
+ * @author EVE
+ *
+ */
+public class TokenUtils {
+ /**
+ * Simple implementation of TokenSplitter for removing double-quoted
+ * strings.
+ *
+ * @author EVE
+ *
+ */
+ public static class StringTokenSplitter implements TokenSplitter {
+ @Override
+ public IList<String> split(final String input) {
+ return new FunctionalList<>(TokenUtils.removeDQuotedStrings(input));
+ }
+ }
+
+ /*
+ * Patterns and pattern parts.
+ */
+ private static String rPossibleEscapeString = getRegex("possibleStringEscape");
+
+ private static Pattern possibleEscapePatt = Pattern.compile(rPossibleEscapeString);
+
+ private static String rShortEscape = getRegex("shortFormStringEscape");
+ private static String rOctalEscape = getRegex("octalStringEscape");
+ private static String rUnicodeEscape = getRegex("unicodeStringEscape");
+
+ private static String rEscapeString = applyFormat("stringEscape", rShortEscape, rOctalEscape, rUnicodeEscape);
+
+ private static Pattern escapePatt = Pattern.compile(rEscapeString);
+
+ private static String rDoubleQuoteString = applyFormat("doubleQuotes", getRegex("nonStringEscape"),
+ rPossibleEscapeString);
+
+ private static Pattern doubleQuotePatt = Pattern.compile(rDoubleQuoteString);
+
+ private static Pattern quotePatt = getCompiledRegex("unescapedQuote");
+
+ private static Pattern intLitPattern = getCompiledRegex("intLiteral");
+
+ /**
+ * Remove double quoted strings from a string.
+ *
+ * Splits a string around instances of java-style double-quoted strings.
+ *
+ * @param inp
+ * The string to split.
+ *
+ * @return An list containing alternating bits of the string and the
+ * embedded double-quoted strings that separated them.
+ */
+ public static List<String> removeDQuotedStrings(final String inp) {
+ if (inp == null) throw new NullPointerException("inp must not be null");
+
+ /*
+ * What we need for piece-by-piece string building
+ */
+ StringBuffer work = new StringBuffer();
+ final List<String> res = new LinkedList<>();
+
+ /*
+ * Matcher for proper strings and single quotes.
+ */
+ final Matcher mt = doubleQuotePatt.matcher(inp);
+ final Matcher corr = quotePatt.matcher(inp);
+
+ if (corr.find() && !corr.find()) {
+ /*
+ * There's a unmatched opening quote with no strings.
+ */
+ final String msg = String.format(
+ "Unclosed string literal '%s'. Opening quote was at position %d", inp,
+ inp.indexOf("\""));
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ while (mt.find()) {
+ /*
+ * Remove the string until the quoted string.
+ */
+ mt.appendReplacement(work, "");
+
+ /*
+ * Add the string preceding the double-quoted string and
+ * the double-quoted string to the list.
+ */
+ res.add(work.toString());
+ res.add(mt.group(1));
+
+ /*
+ * Renew the buffer.
+ */
+ work = new StringBuffer();
+ }
+
+ /*
+ * Grab the remainder of the string.
+ */
+ mt.appendTail(work);
+ final String tail = work.toString();
+
+ if (tail.contains("\"")) {
+ /*
+ * There's a unmatched opening quote with at least one
+ * string.
+ */
+ final String msg = String.format(
+ "Unclosed string literal '%s'. Opening quote was at position %d", inp,
+ inp.lastIndexOf("\""));
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ /*
+ * Only add an empty tail if the string was empty.
+ */
+ if (!tail.equals("") || res.isEmpty()) {
+ res.add(tail);
+ }
+
+ return res;
+ }
+
+ /**
+ * Replace escape characters with their actual equivalents.
+ *
+ * @param inp
+ * The string to replace escape sequences in.
+ *
+ * @return The string with escape sequences replaced by their equivalent
+ * characters.
+ */
+ public static String descapeString(final String inp) {
+ if (inp == null) throw new NullPointerException("inp must not be null");
+
+ /*
+ * Prepare the buffer and escape finder.
+ */
+ final StringBuffer work = new StringBuffer();
+ final Matcher possibleEscapeFinder = possibleEscapePatt.matcher(inp);
+ final Matcher escapeFinder = escapePatt.matcher(inp);
+
+ while (possibleEscapeFinder.find()) {
+ if (!escapeFinder.find()) {
+ /*
+ * Found a possible escape that isn't actually an
+ * escape.
+ */
+ final String msg = String.format("Illegal escape sequence '%s' at position %d",
+ possibleEscapeFinder.group(), possibleEscapeFinder.start());
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ final String escapeSeq = escapeFinder.group();
+
+ /*
+ * Convert the escape to a string.
+ */
+ String escapeRep = "";
+ switch (escapeSeq) {
+ case "\\b":
+ escapeRep = "\b";
+ break;
+ case "\\t":
+ escapeRep = "\t";
+ break;
+ case "\\n":
+ escapeRep = "\n";
+ break;
+ case "\\f":
+ escapeRep = "\f";
+ break;
+ case "\\r":
+ escapeRep = "\r";
+ break;
+ case "\\\"":
+ escapeRep = "\"";
+ break;
+ case "\\'":
+ escapeRep = "'";
+ break;
+ case "\\\\":
+ /*
+ * Skip past the second slash.
+ */
+ possibleEscapeFinder.find();
+ escapeRep = "\\";
+ break;
+ default:
+ if (escapeSeq.startsWith("u")) {
+ escapeRep = handleUnicodeEscape(escapeSeq.substring(1));
+ } else {
+ escapeRep = handleOctalEscape(escapeSeq);
+ }
+ }
+
+ escapeFinder.appendReplacement(work, escapeRep);
+ }
+
+ escapeFinder.appendTail(work);
+
+ return work.toString();
+ }
+
+ /*
+ * Handle a unicode codepoint.
+ */
+ private static String handleUnicodeEscape(final String seq) {
+ try {
+ final int codepoint = Integer.parseInt(seq, 16);
+
+ return new String(Character.toChars(codepoint));
+ } catch (final IllegalArgumentException iaex) {
+ final String msg = String.format("'%s' is not a valid Unicode escape sequence'", seq);
+
+ final IllegalArgumentException reiaex = new IllegalArgumentException(msg);
+
+ reiaex.initCause(iaex);
+
+ throw reiaex;
+ }
+ }
+
+ /*
+ * Handle a octal codepoint.
+ */
+ private static String handleOctalEscape(final String seq) {
+ try {
+ final int codepoint = Integer.parseInt(seq, 8);
+
+ if (codepoint > 255) {
+ final String msg = String
+ .format("'%d' is outside the range of octal escapes', codepoint");
+
+ throw new IllegalArgumentException(msg);
+ }
+
+ return new String(Character.toChars(codepoint));
+ } catch (final IllegalArgumentException iaex) {
+ final String msg = String.format("'%s' is not a valid octal escape sequence'", seq);
+
+ final IllegalArgumentException reiaex = new IllegalArgumentException(msg);
+
+ reiaex.initCause(iaex);
+
+ throw reiaex;
+ }
+ }
+
+ /**
+ * Check if a given string would be successfully converted to a double
+ * by {@link Double#parseDouble(String)}.
+ *
+ * @param inp
+ * The string to check.
+ * @return Whether the string is a valid double or not.
+ */
+ public static boolean isDouble(final String inp) {
+ return DoubleMatcher.doubleLiteral.matcher(inp).matches();
+ }
+
+ /**
+ * Check if a given string would be successfully converted to a integer
+ * by {@link Integer#parseInt(String)}.
+ *
+ * NOTE: This only checks syntax. Using values out of the range of
+ * integers will still cause errors.
+ *
+ * @param inp
+ * The input to check.
+ * @return Whether the string is a valid integer or not.
+ */
+ public static boolean isInt(final String inp) {
+ try {
+ Integer.parseInt(inp);
+ return true;
+ } catch (NumberFormatException nfex) {
+ return false;
+ }
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java b/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java
new file mode 100644
index 0000000..90141ef
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/TreeConstructor.java
@@ -0,0 +1,125 @@
+package bjc.utils.parserutils;
+
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import bjc.utils.data.IHolder;
+import bjc.utils.data.IPair;
+import bjc.utils.data.ITree;
+import bjc.utils.data.Identity;
+import bjc.utils.data.Pair;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Creates a parse tree from a postfix expression
+ *
+ * @author ben
+ *
+ */
+public class TreeConstructor {
+ /**
+ * Alias interface for special operator types.
+ *
+ * @param <TokenType>
+ * The token type of the tree.
+ */
+ public interface QueueFlattener<TokenType> extends Function<Deque<ITree<TokenType>>, ITree<TokenType>> {
+
+ }
+
+ /*
+ * Alias for constructor state.
+ */
+ static final class ConstructorState<TokenType> extends Pair<Deque<ITree<TokenType>>, ITree<TokenType>> {
+ public ConstructorState(final Deque<ITree<TokenType>> left, final ITree<TokenType> right) {
+ super(left, right);
+ }
+
+ public ConstructorState(final IPair<Deque<ITree<TokenType>>, ITree<TokenType>> par) {
+ super(par.getLeft(), par.getRight());
+ }
+ }
+
+ /**
+ * Construct a tree from a list of tokens in postfix notation
+ *
+ * Only binary operators are accepted.
+ *
+ * @param <TokenType>
+ * The elements of the parse tree
+ * @param tokens
+ * The list of tokens to build a tree from
+ * @param isOperator
+ * The predicate to use to determine if something is a
+ * operator
+ * @return A AST from the expression
+ */
+ public static <TokenType> ITree<TokenType> constructTree(final IList<TokenType> tokens,
+ final Predicate<TokenType> isOperator) {
+ /*
+ * Construct a tree with no special operators
+ */
+ return constructTree(tokens, isOperator, op -> false, null);
+ }
+
+ /**
+ * Construct a tree from a list of tokens in postfix notation.
+ *
+ * Only binary operators are accepted by default. Use the last two
+ * parameters to handle non-binary operators.
+ *
+ * @param <TokenType>
+ * The elements of the parse tree.
+ *
+ * @param tokens
+ * The list of tokens to build a tree from.
+ *
+ * @param isOperator
+ * The predicate to use to determine if something is a
+ * operator.
+ *
+ * @param isSpecialOperator
+ * The predicate to use to determine if an operator needs
+ * special handling.
+ *
+ * @param handleSpecialOperator
+ * The function to use to handle special case operators.
+ *
+ * @return A AST from the expression
+ *
+ */
+ public static <TokenType> ITree<TokenType> constructTree(final IList<TokenType> tokens,
+ final Predicate<TokenType> isOperator, final Predicate<TokenType> isSpecialOperator,
+ final Function<TokenType, QueueFlattener<TokenType>> handleSpecialOperator) {
+ /*
+ * Make sure our parameters are valid
+ */
+ if (tokens == null)
+ throw new NullPointerException("Tokens must not be null");
+ else if (isOperator == null)
+ throw new NullPointerException("Operator predicate must not be null");
+ else if (isSpecialOperator == null)
+ throw new NullPointerException("Special operator determiner must not be null");
+
+ /*
+ * Here is the state for the tree construction
+ */
+ final IHolder<ConstructorState<TokenType>> initialState = new Identity<>(
+ new ConstructorState<>(new LinkedList<>(), null));
+
+ /*
+ * Transform each of the tokens
+ */
+ tokens.forEach(new TokenTransformer<>(initialState, isOperator, isSpecialOperator,
+ handleSpecialOperator));
+
+ /*
+ * Grab the tree from the state
+ */
+ return initialState.unwrap(pair -> {
+ return pair.getRight();
+ });
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java b/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java
new file mode 100644
index 0000000..552b471
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/defines/IteratedDefine.java
@@ -0,0 +1,48 @@
+package bjc.utils.parserutils.defines;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.function.UnaryOperator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.data.CircularIterator;
+
+public class IteratedDefine implements UnaryOperator<String> {
+ private Pattern patt;
+
+ private Iterator<String> repls;
+
+ /**
+ * Create a new iterated define.
+ *
+ * @param pattern
+ * The pattern to use for matching.
+ * @param circular
+ * Whether or not to loop through the list of replacers, or just
+ * repeat the last one.
+ * @param replacers
+ * The set of replacers to use.
+ */
+ public IteratedDefine(Pattern pattern, boolean circular, String... replacers) {
+ patt = pattern;
+
+ repls = new CircularIterator<>(Arrays.asList(replacers), circular);
+ }
+
+ @Override
+ public String apply(String ln) {
+ Matcher mat = patt.matcher(ln);
+ StringBuffer sb = new StringBuffer();
+
+ while(mat.find()) {
+ String repl = repls.next();
+
+ mat.appendReplacement(sb, repl);
+ }
+
+ mat.appendTail(sb);
+
+ return sb.toString();
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java b/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java
new file mode 100644
index 0000000..42866c2
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/defines/SimpleDefine.java
@@ -0,0 +1,23 @@
+package bjc.utils.parserutils.defines;
+
+import java.util.function.UnaryOperator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SimpleDefine implements UnaryOperator<String> {
+ private Pattern patt;
+ private String repl;
+
+ public SimpleDefine(Pattern pattern, String replace) {
+ patt = pattern;
+
+ repl = replace;
+ }
+
+ @Override
+ public String apply(String line) {
+ Matcher mat = patt.matcher(line);
+
+ return mat.replaceAll(repl);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java
new file mode 100644
index 0000000..071afb4
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterException.java
@@ -0,0 +1,21 @@
+package bjc.utils.parserutils.delims;
+
+/**
+ * The superclass for exceptions thrown during sequence delimitation.
+ */
+public class DelimiterException extends RuntimeException {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 2079514406049040888L;
+
+ /**
+ * Create a new generic delimiter exception.
+ *
+ * @param res
+ * The reason for this exception.
+ */
+ public DelimiterException(final String res) {
+ super(res);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java
new file mode 100644
index 0000000..b1d8597
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/DelimiterGroup.java
@@ -0,0 +1,593 @@
+package bjc.utils.parserutils.delims;
+
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import bjc.utils.data.IPair;
+import bjc.utils.data.ITree;
+import bjc.utils.data.Pair;
+import bjc.utils.data.Tree;
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * Represents a possible delimiter group to match.
+ *
+ * @author EVE
+ *
+ * @param <T>
+ * The type of items in the sequence.
+ */
+public class DelimiterGroup<T> {
+ /**
+ * Represents an instance of a delimiter group.
+ *
+ * @author EVE
+ *
+ */
+ public class OpenGroup {
+ /*
+ * The contents of this group.
+ */
+ private final Deque<ITree<T>> contents;
+
+ /*
+ * The contents of the current subgroup.
+ */
+ private IList<ITree<T>> currentGroup;
+
+ /*
+ * The token that opened the group, and any opening parameters.
+ */
+ private final T opener;
+ private final T[] params;
+
+ /**
+ * Create a new instance of a delimiter group.
+ *
+ * @param open
+ * The item that opened this group.
+ *
+ * @param parms
+ * Any parameters from the opener.
+ */
+ public OpenGroup(final T open, final T[] parms) {
+ opener = open;
+ params = parms;
+
+ contents = new LinkedList<>();
+
+ currentGroup = new FunctionalList<>();
+ }
+
+ /**
+ * Add an item to this group instance.
+ *
+ * @param itm
+ * The item to add to this group instance.
+ */
+ public void addItem(final ITree<T> itm) {
+ currentGroup.add(itm);
+ }
+
+ /**
+ * Mark a subgroup.
+ *
+ * @param marker
+ * The item that indicated this subgroup.
+ *
+ * @param chars
+ * The characteristics for building the tree.
+ */
+ public void markSubgroup(final T marker, final SequenceCharacteristics<T> chars) {
+ /*
+ * Add all of the contents to the subgroup.
+ */
+ final ITree<T> subgroupContents = new Tree<>(chars.contents);
+ for (final ITree<T> itm : currentGroup) {
+ subgroupContents.addChild(itm);
+ }
+
+ /*
+ * Handle subordinate sub-groups.
+ */
+ while (!contents.isEmpty()) {
+ final ITree<T> possibleSubordinate = contents.peek();
+
+ /*
+ * Subordinate lower priority subgroups.
+ */
+ if (possibleSubordinate.getHead().equals(chars.subgroup)) {
+ final T otherMarker = possibleSubordinate.getChild(1).getHead();
+
+ if (subgroups.get(marker) > subgroups.get(otherMarker)) {
+ subgroupContents.prependChild(contents.pop());
+ } else {
+ break;
+ }
+ } else {
+ subgroupContents.prependChild(contents.pop());
+ }
+ }
+
+ final Tree<T> subgroup = new Tree<>(chars.subgroup, subgroupContents, new Tree<>(marker));
+
+ contents.push(subgroup);
+
+ currentGroup = new FunctionalList<>();
+ }
+
+ /**
+ * Convert this group into a tree.
+ *
+ * @param closer
+ * The item that closed this group.
+ *
+ * @param chars
+ * The characteristics for building the tree.
+ *
+ * @return This group as a tree.
+ */
+ public ITree<T> toTree(final T closer, final SequenceCharacteristics<T> chars) {
+ /*
+ * Mark any implied subgroups.
+ */
+ if (impliedSubgroups.containsKey(closer)) {
+ markSubgroup(impliedSubgroups.get(closer), chars);
+ }
+
+ final ITree<T> res = new Tree<>(chars.contents);
+
+ /*
+ * Add either the contents of the current group,
+ * or subgroups if they're their.
+ */
+ if (contents.isEmpty()) {
+ currentGroup.forEach(res::addChild);
+ } else {
+ while (!contents.isEmpty()) {
+ res.prependChild(contents.poll());
+ }
+
+ currentGroup.forEach(res::addChild);
+ }
+
+ return new Tree<>(groupName, new Tree<>(opener), res, new Tree<>(closer));
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("OpenGroup [contents=");
+ builder.append(contents);
+ builder.append(", currentGroup=");
+ builder.append(currentGroup);
+ builder.append(", opener=");
+ builder.append(opener);
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+ /**
+ * Check if a group is excluded at the top level of this group.
+ *
+ * @param groupName
+ * The group to check.
+ *
+ * @return Whether or not the provided group is excluded.
+ */
+ public boolean excludes(final T groupName) {
+ return topLevelExclusions.contains(groupName);
+ }
+
+ /**
+ * Check if the provided delimiter would close this group.
+ *
+ * @param del
+ * The string to check as a closing delimiter.
+ *
+ * @return Whether or not the provided delimiter closes this
+ * group.
+ */
+ public boolean isClosing(final T del) {
+ if (closingDelimiters.contains(del)) return true;
+
+ for (final BiPredicate<T, T[]> pred : predClosers) {
+ if (pred.test(del, params)) return true;
+ }
+
+ return closingDelimiters.contains(del);
+ }
+
+ /**
+ * Get the name of the group this is an instance of.
+ *
+ * @return The name of the group this is an instance of.
+ */
+ public T getName() {
+ return groupName;
+ }
+
+ /**
+ * Get the groups that aren't allowed at all in this group.
+ *
+ * @return The groups that aren't allowed at all in this group.
+ */
+ public Set<T> getNestingExclusions() {
+ return groupExclusions;
+ }
+
+ /**
+ * Get the groups that are allowed to open anywhere inside this
+ * group.
+ *
+ * @return The groups allowed to open anywhere inside this
+ * group.
+ */
+ public Map<T, T> getNestingOpeners() {
+ return nestedOpenDelimiters;
+ }
+
+ /**
+ * Checks if a given token marks a subgroup.
+ *
+ * @param tok
+ * The token to check.
+ *
+ * @return Whether or not the token marks a subgroup.
+ */
+ public boolean marksSubgroup(final T tok) {
+ return subgroups.containsKey(tok);
+ }
+
+ /**
+ * Checks if a given token opens a group.
+ *
+ * @param marker
+ * The token to check.
+ *
+ * @return The name of the group T opens, or null if it doesn't
+ * open one.
+ */
+ public IPair<T, T[]> doesOpen(final T marker) {
+ if (openDelimiters.containsKey(marker)) return new Pair<>(openDelimiters.get(marker), null);
+
+ for (final Function<T, IPair<T, T[]>> pred : predOpeners) {
+ final IPair<T, T[]> par = pred.apply(marker);
+
+ if (par.getLeft() != null) return par;
+ }
+
+ return new Pair<>(null, null);
+ }
+
+ /**
+ * Check if this group starts a new nesting scope.
+ *
+ * @return Whether this group starts a new nesting scope.
+ */
+ public boolean isForgetful() {
+ return forgetful;
+ }
+ }
+
+ /**
+ * The name of this delimiter group.
+ */
+ public final T groupName;
+
+ /*
+ * The delimiters that open groups at the top level of this group.
+ */
+ private final Map<T, T> openDelimiters;
+
+ /*
+ * The delimiters that open groups inside of this group.
+ */
+ private final Map<T, T> nestedOpenDelimiters;
+
+ /*
+ * The delimiters that close this group.
+ */
+ private final Set<T> closingDelimiters;
+
+ /*
+ * The groups that can't occur in the top level of this group.
+ */
+ private final Set<T> topLevelExclusions;
+
+ /*
+ * The groups that can't occur anywhere inside this group.
+ */
+ private final Set<T> groupExclusions;
+
+ /*
+ * Mapping from sub-group delimiters, to any sub-groups enclosed in
+ * them.
+ */
+ private final Map<T, Integer> subgroups;
+
+ /*
+ * Subgroups implied by a particular closing delimiter
+ */
+ private final Map<T, T> impliedSubgroups;
+
+ /*
+ * Allows more complex openings
+ */
+ private final List<Function<T, IPair<T, T[]>>> predOpeners;
+
+ /*
+ * Allow more complex closings
+ */
+ private final List<BiPredicate<T, T[]>> predClosers;
+
+ /*
+ * Whether or not this group starts a new nesting set.
+ */
+ private boolean forgetful;
+
+ /**
+ * Create a new empty delimiter group.
+ *
+ * @param name
+ * The name of the delimiter group
+ */
+ public DelimiterGroup(final T name) {
+ if (name == null) throw new NullPointerException("Group name must not be null");
+
+ groupName = name;
+
+ openDelimiters = new HashMap<>();
+ nestedOpenDelimiters = new HashMap<>();
+
+ closingDelimiters = new HashSet<>();
+
+ topLevelExclusions = new HashSet<>();
+ groupExclusions = new HashSet<>();
+
+ subgroups = new HashMap<>();
+ impliedSubgroups = new HashMap<>();
+
+ predOpeners = new LinkedList<>();
+ predClosers = new LinkedList<>();
+ }
+
+ /**
+ * Adds one or more delimiters that close this group.
+ *
+ * @param closers
+ * Delimiters that close this group.
+ */
+ @SafeVarargs
+ public final void addClosing(final T... closers) {
+ final List<T> closerList = Arrays.asList(closers);
+
+ for (final T closer : closerList) {
+ if (closer == null)
+ throw new NullPointerException("Closing delimiter must not be null");
+ else if (closer.equals(""))
+ /*
+ * We can do this because equals works on
+ * arbitrary objects, not just those of the same
+ * type.
+ */
+ throw new IllegalArgumentException("Empty string is not a valid exclusion");
+ else {
+ closingDelimiters.add(closer);
+ }
+ }
+ }
+
+ /**
+ * Adds one or more groups that cannot occur in the top level of this
+ * group.
+ *
+ * @param exclusions
+ * The groups forbidden in the top level of this group.
+ */
+ @SafeVarargs
+ public final void addTopLevelForbid(final T... exclusions) {
+ for (final T exclusion : exclusions) {
+ if (exclusion == null)
+ throw new NullPointerException("Exclusion must not be null");
+ else if (exclusion.equals(""))
+ /*
+ * We can do this because equals works on
+ * arbitrary objects, not just those of the same
+ * type.
+ */
+ throw new IllegalArgumentException("Empty string is not a valid exclusion");
+ else {
+ topLevelExclusions.add(exclusion);
+ }
+ }
+ }
+
+ /**
+ * Adds one or more groups that cannot occur at all in this group.
+ *
+ * @param exclusions
+ * The groups forbidden inside this group.
+ */
+ @SafeVarargs
+ public final void addGroupForbid(final T... exclusions) {
+ for (final T exclusion : exclusions) {
+ if (exclusion == null)
+ throw new NullPointerException("Exclusion must not be null");
+ else if (exclusion.equals(""))
+ /*
+ * We can do this because equals works on
+ * arbitrary objects, not just those of the same
+ * type.
+ */
+ throw new IllegalArgumentException("Empty string is not a valid exclusion");
+ else {
+ groupExclusions.add(exclusion);
+ }
+ }
+ }
+
+ /**
+ * Adds sub-group markers to this group.
+ *
+ * @param subgroup
+ * The token to mark a sub-group.
+ *
+ * @param priority
+ * The priority of this sub-group.
+ */
+ public void addSubgroup(final T subgroup, final int priority) {
+ if (subgroup == null) throw new NullPointerException("Subgroup marker must not be null");
+
+ subgroups.put(subgroup, priority);
+ }
+
+ /**
+ * Adds a marker that opens a group at the top level of this group.
+ *
+ * @param opener
+ * The marker that opens the group.
+ *
+ * @param group
+ * The group opened by the marker.
+ */
+ public void addOpener(final T opener, final T group) {
+ if (opener == null) throw new NullPointerException("Opener must not be null");
+ else if (group == null) throw new NullPointerException("Group to open must not be null");
+
+ openDelimiters.put(opener, group);
+ }
+
+ /**
+ * Adds a marker that opens a group inside of this group.
+ *
+ * @param opener
+ * The marker that opens the group.
+ *
+ * @param group
+ * The group opened by the marker.
+ */
+ public void addNestedOpener(final T opener, final T group) {
+ if (opener == null) throw new NullPointerException("Opener must not be null");
+ else if (group == null) throw new NullPointerException("Group to open must not be null");
+
+ nestedOpenDelimiters.put(opener, group);
+ }
+
+ /**
+ * Mark a closing delimiter as implying a subgroup.
+ *
+ * @param closer
+ * The closing delimiter.
+ *
+ * @param subgroup
+ * The subgroup to imply.
+ */
+ public void implySubgroup(final T closer, final T subgroup) {
+ if (closer == null) throw new NullPointerException("Closer must not be null");
+ else if (subgroup == null) throw new NullPointerException("Subgroup must not be null");
+ else if (!closingDelimiters.contains(closer)) throw new IllegalArgumentException(String.format("No closing delimiter '%s' defined", closer));
+ else if (!subgroups.containsKey(subgroup)) throw new IllegalArgumentException(String.format("No subgroup '%s' defined", subgroup));
+
+ impliedSubgroups.put(closer, subgroup);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("(");
+
+ builder.append("groupName=[");
+ builder.append(groupName);
+ builder.append("], ");
+
+ builder.append("closingDelimiters=[");
+ for (final T closer : closingDelimiters) {
+ builder.append(closer + ",");
+ }
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append("]");
+
+ if (topLevelExclusions != null && !topLevelExclusions.isEmpty()) {
+ builder.append(", ");
+ builder.append("topLevelExclusions=[");
+ for (final T exclusion : topLevelExclusions) {
+ builder.append(exclusion + ",");
+ }
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append("]");
+ }
+
+ if (groupExclusions != null && !groupExclusions.isEmpty()) {
+ builder.append(", ");
+ builder.append("groupExclusions=[");
+ for (final T exclusion : groupExclusions) {
+ builder.append(exclusion + ",");
+ }
+ builder.deleteCharAt(builder.length() - 1);
+ builder.append("]");
+ }
+
+ builder.append(" )");
+
+ return builder.toString();
+ }
+
+ /**
+ * Open an instance of this group.
+ *
+ * @param opener
+ * The item that opened this group.
+ *
+ * @param parms
+ * The parameters that opened this group
+ *
+ * @return An opened instance of this group.
+ */
+ public OpenGroup open(final T opener, final T[] parms) {
+ return new OpenGroup(opener, parms);
+ }
+
+ /**
+ * Adds a predicated opener to the top level of this group.
+ *
+ * @param pred
+ * The predicate that defines the opener and its
+ * parameters.
+ */
+ public void addPredOpener(final Function<T, IPair<T, T[]>> pred) {
+ predOpeners.add(pred);
+ }
+
+ /**
+ * Adds a predicated closer to the top level of this group.
+ *
+ * @param pred
+ * The predicate that defines the closer.
+ */
+ public void addPredCloser(final BiPredicate<T, T[]> pred) {
+ predClosers.add(pred);
+ }
+
+ /**
+ * Set whether or not this group starts a new nesting set.
+ *
+ * @param forgetful
+ * Whether this group starts a new nesting set.
+ */
+ public void setForgetful(final boolean forgetful) {
+ this.forgetful = forgetful;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java b/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java
new file mode 100644
index 0000000..4b29949
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/RegexCloser.java
@@ -0,0 +1,33 @@
+package bjc.utils.parserutils.delims;
+
+import java.util.function.BiPredicate;
+
+/**
+ * A predicated closer for use with {@link RegexOpener}.
+ *
+ * @author bjculkin
+ *
+ */
+public class RegexCloser implements BiPredicate<String, String[]> {
+ private final String rep;
+
+ /**
+ * Create a new regex closer.
+ *
+ * @param closer
+ * The format string to use for closing.
+ */
+ public RegexCloser(final String closer) {
+ rep = closer;
+ }
+
+ @Override
+ public boolean test(final String closer, final String[] params) {
+ /*
+ * Confirm passing an array instead of a single var-arg.
+ */
+ final String work = String.format(rep, (Object[]) params);
+
+ return work.equals(closer);
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java b/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java
new file mode 100644
index 0000000..ee93b73
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/RegexOpener.java
@@ -0,0 +1,54 @@
+package bjc.utils.parserutils.delims;
+
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import bjc.utils.data.IPair;
+import bjc.utils.data.Pair;
+
+/**
+ * A predicated opener for use with {@link RegexCloser}
+ *
+ * @author bjculkin
+ *
+ */
+public class RegexOpener implements Function<String, IPair<String, String[]>> {
+ private final String name;
+
+ private final Pattern patt;
+
+ /**
+ * Create a new regex opener.
+ *
+ * @param groupName
+ * The name of the opened group.
+ *
+ * @param groupRegex
+ * The regex that matches the opener.
+ */
+ public RegexOpener(final String groupName, final String groupRegex) {
+ name = groupName;
+
+ patt = Pattern.compile(groupRegex);
+ }
+
+ @Override
+ public IPair<String, String[]> apply(final String str) {
+ final Matcher m = patt.matcher(str);
+
+ if (m.matches()) {
+ final int numGroups = m.groupCount();
+
+ final String[] parms = new String[numGroups + 1];
+
+ for (int i = 0; i <= numGroups; i++) {
+ parms[i] = m.group(i);
+ }
+
+ return new Pair<>(name, parms);
+ }
+
+ return new Pair<>(null, null);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java b/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java
new file mode 100644
index 0000000..882b4c5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/SequenceCharacteristics.java
@@ -0,0 +1,93 @@
+package bjc.utils.parserutils.delims;
+
+/**
+ * Marks the parameters for building a sequence tree.
+ *
+ * @author EVE
+ *
+ * @param <T>
+ * The type of item in the tree.
+ */
+public class SequenceCharacteristics<T> {
+ /**
+ * The item to mark the root of the tree.
+ */
+ public final T root;
+
+ /**
+ * The item to mark the contents of a group/subgroup.
+ */
+
+ public final T contents;
+
+ /**
+ * The item to mark a subgroup.
+ */
+ public final T subgroup;
+
+ /**
+ * Create a new set of parameters for building a tree.
+ *
+ * @param root
+ * The root marker.
+ * @param contents
+ * The group/subgroup contents marker.
+ * @param subgroup
+ * The subgroup marker.
+ */
+ public SequenceCharacteristics(final T root, final T contents, final T subgroup) {
+ this.root = root;
+ this.contents = contents;
+ this.subgroup = subgroup;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+
+ result = prime * result + (contents == null ? 0 : contents.hashCode());
+ result = prime * result + (root == null ? 0 : root.hashCode());
+ result = prime * result + (subgroup == null ? 0 : subgroup.hashCode());
+
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof SequenceCharacteristics)) return false;
+
+ final SequenceCharacteristics<?> other = (SequenceCharacteristics<?>) obj;
+
+ if (contents == null) {
+ if (other.contents != null) return false;
+ } else if (!contents.equals(other.contents)) return false;
+
+ if (root == null) {
+ if (other.root != null) return false;
+ } else if (!root.equals(other.root)) return false;
+
+ if (subgroup == null) {
+ if (other.subgroup != null) return false;
+ } else if (!subgroup.equals(other.subgroup)) return false;
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("SequenceCharacteristics [root=");
+ builder.append(root == null ? "(null)" : root);
+ builder.append(", contents=");
+ builder.append(contents == null ? "(null)" : contents);
+ builder.append(", subgroup=");
+ builder.append(subgroup == null ? "(null)" : subgroup);
+ builder.append("]");
+
+ return builder.toString();
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java
new file mode 100644
index 0000000..ccfaffb
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/SequenceDelimiter.java
@@ -0,0 +1,371 @@
+package bjc.utils.parserutils.delims;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multiset;
+
+import bjc.utils.data.IPair;
+import bjc.utils.data.ITree;
+import bjc.utils.data.Tree;
+import bjc.utils.esodata.PushdownMap;
+import bjc.utils.esodata.SimpleStack;
+import bjc.utils.esodata.Stack;
+import bjc.utils.funcdata.IMap;
+import bjc.utils.funcutils.StringUtils;
+
+/**
+ * Convert linear sequences into trees that represent group structure.
+ *
+ * @author EVE
+ *
+ * @param <T>
+ * The type of items in the sequence.
+ */
+public class SequenceDelimiter<T> {
+ /*
+ * Mapping from group names to actual groups.
+ */
+ private final Map<T, DelimiterGroup<T>> groups;
+
+ /*
+ * The initial group to start with.
+ */
+ private DelimiterGroup<T> initialGroup;
+
+ /**
+ * Create a new sequence delimiter.
+ */
+ public SequenceDelimiter() {
+ groups = new HashMap<>();
+ }
+
+ /**
+ * Convert a linear sequence into a tree that matches the delimiter
+ * structure.
+ *
+ * Essentially, creates a parse tree of the expression against the
+ * following grammar while obeying the defined grouping rules.
+ *
+ * <pre>
+ * <tree> → (<data> | <subgroup> | <group>)*
+ * <subgroup> → <tree> <marker>
+ * <group> → <open> <tree> <close>
+ *
+ * <data> → STRING
+ * <open> → STRING
+ * <close> → STRING
+ * <marker> → STRING
+ * </pre>
+ *
+ * @param chars
+ * The parameters on how to mark certain portions of the
+ * tree.
+ * @param seq
+ * The sequence to delimit.
+ *
+ * @return The sequence as a tree that matches its group structure. Each
+ * node in the tree is either a data node, a subgroup node, or a
+ * group node.
+ *
+ * A data node is a leaf node whose data is the string it
+ * represents.
+ *
+ * A subgroup node is a node with two children, and the name of
+ * the sub-group as its label. The first child is the contents
+ * of the sub-group, and the second is the marker that started
+ * the subgroup. The marker is a leaf node labeled with its
+ * contents, and the contents contains a recursive tree.
+ *
+ * A group node is a node with three children, and the name of
+ * the group as its label. The first child is the opening
+ * delimiter, the second is the group contents, and the third is
+ * the closing delimiter. The delimiters are leaf nodes labeled
+ * with their contents, while the group node contains a
+ * recursive tree.
+ *
+ * @throws DelimiterException
+ * Thrown if something went wrong during sequence
+ * delimitation.
+ *
+ */
+ public ITree<T> delimitSequence(final SequenceCharacteristics<T> chars,
+ @SuppressWarnings("unchecked") final T... seq) throws DelimiterException {
+ if (initialGroup == null) throw new NullPointerException("Initial group must be specified.");
+ else if (chars == null) throw new NullPointerException("Sequence characteristics must not be null");
+
+ /*
+ * The stack of opened and not yet closed groups.
+ */
+ final Stack<DelimiterGroup<T>.OpenGroup> groupStack = new SimpleStack<>();
+
+ /*
+ * Open initial group.
+ */
+ groupStack.push(initialGroup.open(chars.root, null));
+
+ /*
+ * Groups that aren't allowed to be opened at the moment.
+ */
+ final Stack<Multiset<T>> forbiddenDelimiters = new SimpleStack<>();
+ forbiddenDelimiters.push(HashMultiset.create());
+
+ /*
+ * Groups that are allowed to be opened at the moment.
+ */
+ final Stack<Multimap<T, T>> allowedDelimiters = new SimpleStack<>();
+ allowedDelimiters.push(HashMultimap.create());
+
+ /*
+ * Map of who forbid what for debugging purposes.
+ */
+ final IMap<T, T> whoForbid = new PushdownMap<>();
+
+ /*
+ * Process each member of the sequence.
+ */
+ for (int i = 0; i < seq.length; i++) {
+ final T tok = seq[i];
+
+ /*
+ * Check if this token could open a group.
+ */
+ final IPair<T, T[]> possibleOpenPar = groupStack.top().doesOpen(tok);
+ T possibleOpen = possibleOpenPar.getLeft();
+
+ if (possibleOpen == null) {
+ /*
+ * Handle nested openers.
+ *
+ * Local openers take priority over nested ones
+ * if they overlap.
+ */
+ if (allowedDelimiters.top().containsKey(tok)) {
+ possibleOpen = allowedDelimiters.top().get(tok).iterator().next();
+ }
+ }
+
+ /*
+ * If we have an opening delimiter, handle it.
+ */
+ if (possibleOpen != null) {
+ final DelimiterGroup<T> group = groups.get(possibleOpen);
+
+ /*
+ * Error on groups that can't open in this
+ * context.
+ *
+ * This means groups that can't occur at the
+ * top-level of this group, as well as nested
+ * exclusions from all enclosing groups.
+ */
+ if (isForbidden(groupStack, forbiddenDelimiters, possibleOpen)) {
+ T forbiddenBy;
+
+ if (whoForbid.containsKey(tok)) {
+ forbiddenBy = whoForbid.get(tok);
+ } else {
+ forbiddenBy = groupStack.top().getName();
+ }
+
+ final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then");
+
+ final String fmt = "Group '%s' can't be opened in this context. (forbidden by '%s')\nContext Stack: %s";
+
+ throw new DelimiterException(String.format(fmt, group, forbiddenBy, ctxList));
+ }
+
+ /*
+ * Add an open group.
+ */
+ final DelimiterGroup<T>.OpenGroup open = group.open(tok, possibleOpenPar.getRight());
+ groupStack.push(open);
+
+ /*
+ * Handle 'forgetful' groups that reset nesting
+ */
+ if (open.isForgetful()) {
+ allowedDelimiters.push(HashMultimap.create());
+ forbiddenDelimiters.push(HashMultiset.create());
+ }
+
+ /*
+ * Add the nested opens from this group.
+ */
+ final Multimap<T, T> currentAllowed = allowedDelimiters.top();
+ for (final Entry<T, T> opener : open.getNestingOpeners().entrySet()) {
+ currentAllowed.put(opener.getKey(), opener.getValue());
+ }
+
+ /*
+ * Add the nested exclusions from this group
+ */
+ final Multiset<T> currentForbidden = forbiddenDelimiters.top();
+ for (final T exclusion : open.getNestingExclusions()) {
+ currentForbidden.add(exclusion);
+
+ whoForbid.put(exclusion, possibleOpen);
+ }
+ } else if (!groupStack.empty() && groupStack.top().isClosing(tok)) {
+ /*
+ * Close the group.
+ */
+ final DelimiterGroup<T>.OpenGroup closed = groupStack.pop();
+
+ groupStack.top().addItem(closed.toTree(tok, chars));
+
+ /*
+ * Remove nested exclusions from this group.
+ */
+ final Multiset<T> currentForbidden = forbiddenDelimiters.top();
+ for (final T excludedGroup : closed.getNestingExclusions()) {
+ currentForbidden.remove(excludedGroup);
+
+ whoForbid.remove(excludedGroup);
+ }
+
+ /*
+ * Remove the nested opens from this group.
+ */
+ final Multimap<T, T> currentAllowed = allowedDelimiters.top();
+ for (final Entry<T, T> closer : closed.getNestingOpeners().entrySet()) {
+ currentAllowed.remove(closer.getKey(), closer.getValue());
+ }
+
+ /*
+ * Handle 'forgetful' groups that reset nesting.
+ */
+ if (closed.isForgetful()) {
+ allowedDelimiters.drop();
+ forbiddenDelimiters.drop();
+ }
+ } else if (!groupStack.empty() && groupStack.top().marksSubgroup(tok)) {
+ /*
+ * Mark a subgroup.
+ */
+ groupStack.top().markSubgroup(tok, chars);
+ } else {
+ /*
+ * Add an item to the group.
+ */
+ groupStack.top().addItem(new Tree<>(tok));
+ }
+ }
+
+ /*
+ * Error if not all groups were closed.
+ */
+ if (groupStack.size() > 1) {
+ final DelimiterGroup<T>.OpenGroup group = groupStack.top();
+
+ final StringBuilder msgBuilder = new StringBuilder();
+
+ final String closingDelims = StringUtils.toEnglishList(group.getNestingExclusions().toArray(),
+ false);
+
+ final String ctxList = StringUtils.toEnglishList(groupStack.toArray(), "then");
+
+ msgBuilder.append("Unclosed group '");
+ msgBuilder.append(group.getName());
+ msgBuilder.append("'. Expected one of ");
+ msgBuilder.append(closingDelims);
+ msgBuilder.append(" to close it\nOpen groups: ");
+ msgBuilder.append(ctxList);
+
+ final String fmt = "Unclosed group '%s'. Expected one of %s to close it.\nOpen groups: %n";
+
+ throw new DelimiterException(String.format(fmt, group.getName(), closingDelims, ctxList));
+ }
+
+ return groupStack.pop().toTree(chars.root, chars);
+ }
+
+ /*
+ * Check if a group is forbidden to open in a context.
+ */
+ private boolean isForbidden(final Stack<DelimiterGroup<T>.OpenGroup> groupStack,
+ final Stack<Multiset<T>> forbiddenDelimiters, final T groupName) {
+ boolean localForbid;
+
+ /*
+ * Check if a delimiter is locally forbidden.
+ */
+ if (groupStack.empty()) {
+ localForbid = false;
+ } else {
+ localForbid = groupStack.top().excludes(groupName);
+ }
+
+ return localForbid || forbiddenDelimiters.top().contains(groupName);
+ }
+
+ /**
+ * Add a delimiter group.
+ *
+ * @param group
+ * The delimiter group.
+ */
+ public void addGroup(final DelimiterGroup<T> group) {
+ if (group == null) throw new NullPointerException("Group must not be null");
+
+ groups.put(group.groupName, group);
+ }
+
+ /**
+ * Creates and adds a delimiter group using the provided settings.
+ *
+ * @param openers
+ * The tokens that open this group
+ * @param groupName
+ * The name of the group
+ * @param closers
+ * The tokens that close this group
+ */
+ public void addGroup(final T[] openers, final T groupName, @SuppressWarnings("unchecked") final T... closers) {
+ final DelimiterGroup<T> group = new DelimiterGroup<>(groupName);
+
+ group.addClosing(closers);
+
+ addGroup(group);
+
+ for (final T open : openers) {
+ group.addOpener(open, groupName);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append("SequenceDelimiter [");
+
+ if (groups != null) {
+ builder.append("groups=");
+ builder.append(groups);
+ builder.append(",");
+ }
+
+ if (initialGroup != null) {
+ builder.append("initialGroup=");
+ builder.append(initialGroup);
+ }
+
+ builder.append("]");
+
+ return builder.toString();
+ }
+
+ /**
+ * Set the initial group of this delimiter.
+ *
+ * @param initialGroup
+ * The initial group of this delimiter.
+ */
+ public void setInitialGroup(final DelimiterGroup<T> initialGroup) {
+ this.initialGroup = initialGroup;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java b/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java
new file mode 100644
index 0000000..e3eeea5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/delims/StringDelimiter.java
@@ -0,0 +1,31 @@
+package bjc.utils.parserutils.delims;
+
+import bjc.utils.data.ITree;
+
+/**
+ * A sequence delimiter specialized for strings.
+ *
+ * @author EVE
+ *
+ */
+public class StringDelimiter extends SequenceDelimiter<String> {
+
+ /**
+ * Override of
+ * {@link SequenceDelimiter#delimitSequence(SequenceCharacteristics, Object...)}
+ * for ease of use for strings.
+ *
+ * @param seq
+ * The sequence to delimit.
+ *
+ * @return The sequence as a tree.
+ *
+ * @throws DelimiterException
+ * if something went wrong with delimiting the sequence.
+ *
+ * @see SequenceDelimiter
+ */
+ public ITree<String> delimitSequence(final String... seq) throws DelimiterException {
+ return super.delimitSequence(new SequenceCharacteristics<>("root", "contents", "subgroup"), seq);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java
new file mode 100644
index 0000000..4736310
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/ChainTokenSplitter.java
@@ -0,0 +1,50 @@
+package bjc.utils.parserutils.splitter;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A token splitter that chains several other splitters together.
+ *
+ * @author EVE
+ *
+ */
+public class ChainTokenSplitter implements TokenSplitter {
+ private final IList<TokenSplitter> spliters;
+
+ /**
+ * Create a new chain token splitter.
+ */
+ public ChainTokenSplitter() {
+ spliters = new FunctionalList<>();
+ }
+
+ /**
+ * Append a series of splitters to the chain.
+ *
+ * @param splitters
+ * The splitters to append to the chain.
+ */
+ public void appendSplitters(final TokenSplitter... splitters) {
+ spliters.addAll(splitters);
+ }
+
+ /**
+ * Prepend a series of splitters to the chain.
+ *
+ * @param splitters
+ * The splitters to append to the chain.
+ */
+ public void prependSplitters(final TokenSplitter... splitters) {
+ spliters.prependAll(splitters);
+ }
+
+ @Override
+ public IList<String> split(final String input) {
+ final IList<String> initList = new FunctionalList<>(input);
+
+ return spliters.reduceAux(initList, (splitter, strangs) -> {
+ return strangs.flatMap(splitter::split);
+ });
+ }
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java
new file mode 100644
index 0000000..48ddcb4
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/ConfigurableTokenSplitter.java
@@ -0,0 +1,122 @@
+package bjc.utils.parserutils.splitter;
+
+import static bjc.utils.PropertyDB.applyFormat;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import bjc.utils.funcdata.IList;
+
+/**
+ * Split a string into pieces around a regular expression, and offer an easy way
+ * to configure the regular expression.
+ *
+ * @author EVE
+ *
+ */
+public class ConfigurableTokenSplitter extends SimpleTokenSplitter {
+ private final Set<String> simpleDelimiters;
+ private final Set<String> multipleDelimiters;
+ private final Set<String> rRawDelimiters;
+
+ /**
+ * Create a new token splitter with blank configuration.
+ *
+ * @param keepDelims
+ * Whether or not to keep delimiters.
+ */
+ public ConfigurableTokenSplitter(final boolean keepDelims) {
+ super(null, keepDelims);
+
+ /*
+ * Use linked hash-sets to keep items in insertion order.
+ */
+ simpleDelimiters = new LinkedHashSet<>();
+ multipleDelimiters = new LinkedHashSet<>();
+ rRawDelimiters = new LinkedHashSet<>();
+ }
+
+ /**
+ * Add a set of simple delimiters to this splitter.
+ *
+ * Simple delimiters match one occurrence of themselves as literals.
+ *
+ * @param simpleDelims
+ * The simple delimiters to add.
+ */
+ public void addSimpleDelimiters(final String... simpleDelims) {
+ for (final String simpleDelim : simpleDelims) {
+ simpleDelimiters.add(simpleDelim);
+ }
+ }
+
+ /**
+ * Add a set of multiple delimiters to this splitter.
+ *
+ * Multiple delimiters match one or more occurrences of themselves as
+ * literals.
+ *
+ * @param multiDelims
+ * The multiple delimiters to add.
+ */
+ public void addMultiDelimiters(final String... multiDelims) {
+ for (final String multiDelim : multiDelims) {
+ multipleDelimiters.add(multiDelim);
+ }
+ }
+
+ /**
+ * Add a set of raw delimiters to this splitter.
+ *
+ * Raw delimiters match one occurrence of themselves as regular
+ * expressions.
+ *
+ * @param rRawDelims
+ * The raw delimiters to add.
+ */
+ public void addRawDelimiters(final String... rRawDelims) {
+ for (final String rRawDelim : rRawDelims) {
+ rRawDelimiters.add(rRawDelim);
+ }
+ }
+
+ /**
+ * Take the configuration and compile it into a regular expression to
+ * use when splitting.
+ */
+ public void compile() {
+ final StringBuilder rPattern = new StringBuilder();
+
+ for (final String rRawDelimiter : rRawDelimiters) {
+ rPattern.append(applyFormat("rawDelim", rRawDelimiter));
+ }
+
+ for (final String multipleDelimiter : multipleDelimiters) {
+ rPattern.append(applyFormat("multipleDelim", multipleDelimiter));
+ }
+
+ for (final String simpleDelimiter : simpleDelimiters) {
+ rPattern.append(applyFormat("simpleDelim", simpleDelimiter));
+ }
+
+ rPattern.deleteCharAt(rPattern.length() - 1);
+
+ spliter = Pattern.compile(rPattern.toString());
+ }
+
+ @Override
+ public IList<String> split(final String input) {
+ if (spliter == null) throw new IllegalStateException("Must compile splitter before use");
+
+ return super.split(input);
+ }
+
+ @Override
+ public String toString() {
+ final String fmt = "ConfigurableTokenSplitter [simpleDelimiters=%s, multipleDelimiters=%s,"
+ + " rRawDelimiters=%s, spliter=%s]";
+
+ return String.format(fmt, simpleDelimiters, multipleDelimiters, rRawDelimiters, spliter);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java
new file mode 100644
index 0000000..369e7ae
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/ExcludingTokenSplitter.java
@@ -0,0 +1,71 @@
+package bjc.utils.parserutils.splitter;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import bjc.utils.funcdata.FunctionalList;
+import bjc.utils.funcdata.IList;
+
+/**
+ * A token splitter that will not split certain tokens.
+ *
+ * @author EVE
+ *
+ */
+public class ExcludingTokenSplitter implements TokenSplitter {
+ private final Set<String> literalExclusions;
+
+ private final IList<Predicate<String>> predExclusions;
+
+ private final TokenSplitter spliter;
+
+ /**
+ * Create a new excluding token splitter.
+ *
+ * @param splitter
+ * The splitter to apply to non-excluded strings.
+ */
+ public ExcludingTokenSplitter(final TokenSplitter splitter) {
+ spliter = splitter;
+
+ literalExclusions = new HashSet<>();
+
+ predExclusions = new FunctionalList<>();
+ }
+
+ /**
+ * Exclude literal strings from splitting.
+ *
+ * @param exclusions
+ * The strings to exclude from splitting.
+ */
+ public final void addLiteralExclusions(final String... exclusions) {
+ for (final String exclusion : exclusions) {
+ literalExclusions.add(exclusion);
+ }
+ }
+
+ /**
+ * Exclude all of the strings matching any of the predicates from
+ * splitting.
+ *
+ * @param exclusions
+ * The predicates to use for exclusions.
+ */
+ @SafeVarargs
+ public final void addPredicateExclusion(final Predicate<String>... exclusions) {
+ for (final Predicate<String> exclusion : exclusions) {
+ predExclusions.add(exclusion);
+ }
+ }
+
+ @Override
+ public IList<String> split(final String input) {
+ if (literalExclusions.contains(input))
+ return new FunctionalList<>(input);
+ else if (predExclusions.anyMatch(pred -> pred.test(input)))
+ return new FunctionalList<>(input);
+ else return spliter.split(input);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java
new file mode 100644
index 0000000..5d954e0
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/FilteredTokenSplitter.java
@@ -0,0 +1,37 @@
+package bjc.utils.parserutils.splitter;
+
+import java.util.function.Predicate;
+
+import bjc.utils.funcdata.IList;
+
+/**
+ * A token splitter that removes tokens that match a predicate from the stream
+ * of tokens.
+ *
+ * @author bjculkin
+ *
+ */
+public class FilteredTokenSplitter implements TokenSplitter {
+ private TokenSplitter source;
+
+ private Predicate<String> filter;
+
+ /**
+ * Create a new filtered token splitter.
+ *
+ * @param source
+ * The splitter to get tokens from.
+ *
+ * @param filter
+ * The filter to pass tokens through.
+ */
+ public FilteredTokenSplitter(TokenSplitter source, Predicate<String> filter) {
+ this.source = source;
+ this.filter = filter;
+ }
+
+ @Override
+ public IList<String> split(String input) {
+ return source.split(input).getMatching(filter);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java
new file mode 100644
index 0000000..c357886
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/SimpleTokenSplitter.java
@@ -0,0 +1,46 @@
+package bjc.utils.parserutils.splitter;
+
+import java.util.regex.Pattern;
+
+import bjc.utils.funcdata.IList;
+import bjc.utils.functypes.ID;
+import bjc.utils.ioutils.RegexStringEditor;
+
+/**
+ * Splits a string into pieces around a regular expression.
+ *
+ * @author EVE
+ *
+ */
+public class SimpleTokenSplitter implements TokenSplitter {
+ protected Pattern spliter;
+
+ private final boolean keepDelim;
+
+ /**
+ * Create a new simple token splitter.
+ *
+ * @param splitter
+ * The pattern to split around.
+ *
+ * @param keepDelims
+ * Whether or not delimiters should be kept.
+ */
+ public SimpleTokenSplitter(final Pattern splitter, final boolean keepDelims) {
+ spliter = splitter;
+
+ keepDelim = keepDelims;
+ }
+
+ @Override
+ public IList<String> split(final String input) {
+ if (keepDelim)
+ return RegexStringEditor.mapOccurances(input, spliter, ID.id(), ID.id());
+ else return RegexStringEditor.mapOccurances(input, spliter, ID.id(), strang -> "");
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SimpleTokenSplitter [spliter=%s, keepDelim=%s]", spliter, keepDelim);
+ }
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java
new file mode 100644
index 0000000..ddb28a7
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/TokenSplitter.java
@@ -0,0 +1,21 @@
+package bjc.utils.parserutils.splitter;
+
+import bjc.utils.funcdata.IList;
+
+/**
+ * Split a string into a list of pieces.
+ *
+ * @author EVE
+ *
+ */
+public interface TokenSplitter {
+ /**
+ * Split a string into a list of pieces.
+ *
+ * @param input
+ * The string to split.
+ *
+ * @return The pieces of the string.
+ */
+ public IList<String> split(String input);
+}
diff --git a/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java b/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java
new file mode 100644
index 0000000..80490f5
--- /dev/null
+++ b/base/src/main/java/bjc/utils/parserutils/splitter/TransformTokenSplitter.java
@@ -0,0 +1,38 @@
+package bjc.utils.parserutils.splitter;
+
+import java.util.function.UnaryOperator;
+
+import bjc.utils.funcdata.IList;
+
+/**
+ * A token splitter that performs a transform on the tokens from another
+ * splitter.
+ *
+ * @author bjculkin
+ *
+ */
+public class TransformTokenSplitter implements TokenSplitter {
+ private TokenSplitter source;
+
+ private UnaryOperator<String> transform;
+
+ /**
+ * Create a new transforming splitter.
+ *
+ * @param source
+ * The splitter to use as a source.
+ *
+ * @param transform
+ * The transform to apply to tokens.
+ */
+ public TransformTokenSplitter(TokenSplitter source, UnaryOperator<String> transform) {
+ this.source = source;
+ this.transform = transform;
+ }
+
+ @Override
+ public IList<String> split(String input) {
+ return source.split(input).map(transform);
+ }
+
+}