diff options
3 files changed, 704 insertions, 2 deletions
diff --git a/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLFormatter.java b/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLFormatter.java index a119c9b..f9ae49a 100644 --- a/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLFormatter.java +++ b/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLFormatter.java @@ -1,14 +1,202 @@ package bjc.utils.ioutils;
+import java.util.HashMap;
+import java.util.IllegalFormatConversionException;
+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;
-import java.util.regex.Pattern;
-
public class CLFormatter {
+ @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 formatDirective = applyFormat("clFormatDirective", prefixList, formatMod);
+ private static final Pattern pFormatDirective = Pattern.compile(formatDirective);
+
+ private Map<String, Directive> extraDirectives;
+
+ public CLFormatter() {
+ extraDirectives = new HashMap<>();
+ }
+
+ public String formatString(String format, Object... params) {
+ StringBuffer sb = new StringBuffer();
+ Matcher dirMatcher = pFormatDirective.matcher(format);
+
+ /*
+ * Put the parameters where we can easily handle them.
+ */
+ Tape<Object> tParams = new SingleTape(params);
+
+ while(dirMatcher.find()) {
+ dirMatcher.appendReplacement(sb, "");
+
+ String dirName = dirMatcher.group("name");
+ String dirMods = dirMatcher.group("modifiers");
+ String dirParams = dirMatcher.group("params");
+
+ CLParameters arrParams = CLParameters.fromDirective(dirParams.split("(?<!'),"), tParams);
+
+ boolean atMod = false;
+ boolean colonMod = false;
+ if(dirMods != null) {
+ atMod = dirMods.contains("@");
+ colonMod = dirMods.contains(":");
+ }
+
+ switch(dirName) {
+ case "C":
+ handleCDirective(sb, tParams.item(), atMod, colonMod);
+ 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 "R":
+ handleRadixDirective(sb, atMod, colonMod, arrParams, tParams.item());
+ tParams.right();
+ break;
+ default:
+ String msg = String.format("Unknown format directive '%s'", dirName);
+ throw new UnknownFormatConversionException(msg);
+ }
+ }
+
+ dirMatcher.appendTail(sb);
+
+ return sb.toString();
+ }
+
+ private void handleCDirective(StringBuffer buff, Object parm, boolean atMod, boolean colonMod) {
+ if(!(parm instanceof Character)) {
+ throw new IllegalFormatConversionException('C', parm.getClass());
+ }
+
+ char ch = (Character) parm;
+ int codepoint = (int) ch;
+
+ if(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 handleRadixDirective(StringBuffer buff, boolean atMod, boolean colonMod, 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(atMod) {
+ buff.append(NumberUtils.toRoman((Long)val, colonMod));
+ } else if(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');
+
+ /*
+ * 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() > 2) {
+ mincol = params.getIntDefault(1, "minimum column count", 'R', 0);
+ }
+ if(params.length() > 3) {
+ padchar = params.getCharDefault(2, "padding character", 'R', ' ');
+ }
+
+ if(colonMod) {
+ /*
+ * We're doing commas, so check if the two
+ * comma-related parameters were supplied.
+ */
+ int commaInterval = 0;
+ char commaChar = ',';
+ if(params.length() > 3) {
+ commaChar = params.getCharDefault(3, "comma character", 'R', ' ');
+ }
+ if(params.length() > 4) {
+ commaInterval = params.getIntDefault(4, "comma interval", 'R', 0);
+ }
+
+ NumberUtils.toCommaString(val, mincol, padchar, commaInterval, commaChar, atMod, radix);
+ } else {
+ NumberUtils.toNormalString(val, mincol, padchar, atMod, radix);
+ }
+ }
+ }
}
diff --git a/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLParameters.java b/BJC-Utils2/src/main/java/bjc/utils/ioutils/CLParameters.java new file mode 100644 index 0000000..e4bb6fb --- /dev/null +++ b/BJC-Utils2/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/BJC-Utils2/src/main/java/bjc/utils/ioutils/NumberUtils.java b/BJC-Utils2/src/main/java/bjc/utils/ioutils/NumberUtils.java new file mode 100644 index 0000000..1b754e2 --- /dev/null +++ b/BJC-Utils2/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); + } +} |
