diff options
Diffstat (limited to 'base/src/main/java/bjc/utils/ioutils')
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); + } +} |
