summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc/utils/ioutils
diff options
context:
space:
mode:
authorBenjamin J. Culkin <bjculkin@mix.wvu.edu>2017-10-08 22:39:59 -0300
committerBenjamin J. Culkin <bjculkin@mix.wvu.edu>2017-10-08 22:39:59 -0300
commitc82e3b3b2de0633317ec8fc85925e91422820597 (patch)
tree96567416ce23c5ce85601f9cedc3a94bb1c55cba /base/src/main/java/bjc/utils/ioutils
parentb3ac1c8690c3e14c879913e5dcc03a5f5e14876e (diff)
Start splitting into maven modules
Diffstat (limited to 'base/src/main/java/bjc/utils/ioutils')
-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
21 files changed, 2934 insertions, 0 deletions
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);
+ }
+}