package org.ovirt.engine.core.compat; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Date; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; /** * A port of java.util.Formatter to GWT, without GWT non-compatible types (especially Calendar and Precision * calculations) */ public final class FormatterJava { private enum BigDecimalLayoutForm { DECIMAL_FLOAT, SCIENTIFIC } private static class Conversion { // if (arg.TYPE != boolean) return boolean // if (arg != null) return true; else return false; static final char BOOLEAN = 'b'; static final char BOOLEAN_UPPER = 'B'; // Character, Byte, Short, Integer // (and associated primitives due to autoboxing) static final char CHARACTER = 'c'; static final char CHARACTER_UPPER = 'C'; // java.util.Date, java.util.Calendar, long static final char DATE_TIME = 't'; static final char DATE_TIME_UPPER = 'T'; static final char DECIMAL_FLOAT = 'f'; // Byte, Short, Integer, Long, BigInteger // (and associated primitives due to autoboxing) static final char DECIMAL_INTEGER = 'd'; static final char GENERAL = 'g'; static final char GENERAL_UPPER = 'G'; // arg.hashCode() static final char HASHCODE = 'h'; static final char HASHCODE_UPPER = 'H'; static final char HEXADECIMAL_FLOAT = 'a'; static final char HEXADECIMAL_FLOAT_UPPER = 'A'; static final char HEXADECIMAL_INTEGER = 'x'; static final char HEXADECIMAL_INTEGER_UPPER = 'X'; static final char LINE_SEPARATOR = 'n'; static final char OCTAL_INTEGER = 'o'; static final char PERCENT_SIGN = '%'; // Float, Double, BigDecimal // (and associated primitives due to autoboxing) static final char SCIENTIFIC = 'e'; static final char SCIENTIFIC_UPPER = 'E'; // if (arg instanceof Formattable) arg.formatTo() // else arg.toString(); static final char STRING = 's'; static final char STRING_UPPER = 'S'; // Returns true iff the Conversion is applicable to character. static boolean isCharacter(char c) { switch (c) { case CHARACTER: case CHARACTER_UPPER: return true; default: return false; } } // Returns true iff the Conversion is a floating-point type. static boolean isFloat(char c) { switch (c) { case SCIENTIFIC: case SCIENTIFIC_UPPER: case GENERAL: case GENERAL_UPPER: case DECIMAL_FLOAT: case HEXADECIMAL_FLOAT: case HEXADECIMAL_FLOAT_UPPER: return true; default: return false; } } // Returns true iff the Conversion is applicable to all objects. static boolean isGeneral(char c) { switch (c) { case BOOLEAN: case BOOLEAN_UPPER: case STRING: case STRING_UPPER: case HASHCODE: case HASHCODE_UPPER: return true; default: return false; } } // Returns true iff the Conversion is an integer type. static boolean isInteger(char c) { switch (c) { case DECIMAL_INTEGER: case OCTAL_INTEGER: case HEXADECIMAL_INTEGER: case HEXADECIMAL_INTEGER_UPPER: return true; default: return false; } } // Returns true iff the Conversion does not require an argument static boolean isText(char c) { switch (c) { case LINE_SEPARATOR: case PERCENT_SIGN: return true; default: return false; } } static boolean isValid(char c) { return isGeneral(c) || isInteger(c) || isFloat(c) || isText(c) || c == 't' || isCharacter(c); } } private static class DateTime { static final char AM_PM = 'p'; // (am or pm) static final char CENTURY = 'C'; // (00 - 99) // (Sat Nov 04 12:02:33 EST 1999) static final char DATE = 'D'; // (mm/dd/yy) // * static final char LOCALE_TIME = 'X'; // (%H:%M:%S) - parse format? static final char DATE_TIME = 'c'; static final char DAY_OF_MONTH = 'e'; // (1 - 31) -- like d static final char DAY_OF_MONTH_0 = 'd'; // (01 - 31) static final char DAY_OF_YEAR = 'j'; // (001 - 366) static final char HOUR = 'l'; // (1 - 12) -- like I static final char HOUR_0 = 'I'; // (01 - 12) static final char HOUR_OF_DAY = 'k'; // (0 - 23) -- like H static final char HOUR_OF_DAY_0 = 'H'; // (00 - 23) static final char ISO_STANDARD_DATE = 'F'; // (%Y-%m-%d) static final char MILLISECOND = 'L'; // jdk, not in gnu (000 - 999) static final char MILLISECOND_SINCE_EPOCH = 'Q'; // (0 - 99...?) static final char MINUTE = 'M'; // (00 - 59) static final char MONTH = 'm'; // (01 - 12) static final char NAME_OF_DAY = 'A'; // 'A' // Date static final char NAME_OF_DAY_ABBREV = 'a'; // 'a' static final char NAME_OF_MONTH = 'B'; // 'B' static final char NAME_OF_MONTH_ABBREV = 'b'; // 'b' // * static final char ISO_WEEK_OF_YEAR_2 = 'g'; // cross %y %V // * static final char ISO_WEEK_OF_YEAR_4 = 'G'; // cross %Y %V static final char NAME_OF_MONTH_ABBREV_X = 'h'; // -- same b static final char NANOSECOND = 'N'; // (000000000 - 999999999) static final char SECOND = 'S'; // (00 - 60 - leap second) static final char SECONDS_SINCE_EPOCH = 's'; // (0 - 99...?) static final char TIME = 'T'; // (24 hour hh:mm:ss) // Composites static final char TIME_12_HOUR = 'r'; // (hh:mm:ss [AP]M) static final char TIME_24_HOUR = 'R'; // (hh:mm same as %H:%M) // * static final char DAY_OF_WEEK_1 = 'u'; // (1 - 7) Monday // * static final char WEEK_OF_YEAR_SUNDAY = 'U'; // (0 - 53) Sunday+ // * static final char WEEK_OF_YEAR_MONDAY_01 = 'V'; // (01 - 53) Monday+ // * static final char DAY_OF_WEEK_0 = 'w'; // (0 - 6) Sunday // * static final char WEEK_OF_YEAR_MONDAY = 'W'; // (00 - 53) Monday static final char YEAR_2 = 'y'; // (00 - 99) static final char YEAR_4 = 'Y'; // (0000 - 9999) static final char ZONE = 'Z'; // (symbol) static final char ZONE_NUMERIC = 'z'; // (-1200 - +1200) - ls minus? // * static final char LOCALE_DATE = 'x'; // (mm/dd/yy) static boolean isValid(char c) { switch (c) { case HOUR_OF_DAY_0: case HOUR_0: case HOUR_OF_DAY: case HOUR: case MINUTE: case NANOSECOND: case MILLISECOND: case MILLISECOND_SINCE_EPOCH: case AM_PM: case SECONDS_SINCE_EPOCH: case SECOND: case TIME: case ZONE_NUMERIC: case ZONE: // Date case NAME_OF_DAY_ABBREV: case NAME_OF_DAY: case NAME_OF_MONTH_ABBREV: case NAME_OF_MONTH: case CENTURY: case DAY_OF_MONTH_0: case DAY_OF_MONTH: // * case ISO_WEEK_OF_YEAR_2: // * case ISO_WEEK_OF_YEAR_4: case NAME_OF_MONTH_ABBREV_X: case DAY_OF_YEAR: case MONTH: // * case DAY_OF_WEEK_1: // * case WEEK_OF_YEAR_SUNDAY: // * case WEEK_OF_YEAR_MONDAY_01: // * case DAY_OF_WEEK_0: // * case WEEK_OF_YEAR_MONDAY: case YEAR_2: case YEAR_4: // Composites case TIME_12_HOUR: case TIME_24_HOUR: // * case LOCALE_TIME: case DATE_TIME: case DATE: case ISO_STANDARD_DATE: // * case LOCALE_DATE: return true; default: return false; } } } private class FixedString implements FormatString { private final String s; FixedString(String s) { this.s = s; } @Override public int index() { return -2; } @Override public void print(Object arg) throws IOException { a.append(s); } @Override public String toString() { return s; } } private static class Flags { static final Flags ALTERNATE = new Flags(1 << 2); // '#' static final Flags GROUP = new Flags(1 << 6); // ',' static final Flags LEADING_SPACE = new Flags(1 << 4); // ' ' // duplicate declarations from Formattable.java static final Flags LEFT_JUSTIFY = new Flags(1 << 0); // '-' static final Flags NONE = new Flags(0); // '' static final Flags PARENTHESES = new Flags(1 << 7); // '(' // numerics static final Flags PLUS = new Flags(1 << 3); // '+' // indexing static final Flags PREVIOUS = new Flags(1 << 8); // '<' static final Flags UPPERCASE = new Flags(1 << 1); // '^' static final Flags ZERO_PAD = new Flags(1 << 5); // '0' public static Flags parse(String s) { if (s == null || s.isEmpty()) { return Flags.NONE; } char[] ca = s.toCharArray(); Flags f = new Flags(0); for (int i = 0; i < ca.length; i++) { Flags v = parse(ca[i]); if (f.contains(v)) { throw new IllegalArgumentException(v.toString()); } f.add(v); } return f; } // Returns a string representation of the current <tt>Flags</tt>. public static String toString(Flags f) { return f.toString(); } // parse those flags which may be provided by users private static Flags parse(char c) { switch (c) { case '-': return LEFT_JUSTIFY; case '#': return ALTERNATE; case '+': return PLUS; case ' ': return LEADING_SPACE; case '0': return ZERO_PAD; case ',': return GROUP; case '(': return PARENTHESES; case '<': return PREVIOUS; default: throw new IllegalArgumentException(String.valueOf(c)); } } private int flags; private Flags(int f) { flags = f; } public boolean contains(Flags f) { return (flags & f.valueOf()) == f.valueOf(); } public Flags dup() { return new Flags(flags); } public Flags remove(Flags f) { flags &= ~f.valueOf(); return this; } @Override public String toString() { StringBuilder sb = new StringBuilder(); if (contains(LEFT_JUSTIFY)) { sb.append('-'); } if (contains(UPPERCASE)) { sb.append('^'); } if (contains(ALTERNATE)) { sb.append('#'); } if (contains(PLUS)) { sb.append('+'); } if (contains(LEADING_SPACE)) { sb.append(' '); } if (contains(ZERO_PAD)) { sb.append('0'); } if (contains(GROUP)) { sb.append(','); } if (contains(PARENTHESES)) { sb.append('('); } if (contains(PREVIOUS)) { sb.append('<'); } return sb.toString(); } public int valueOf() { return flags; } Flags add(Flags f) { flags |= f.valueOf(); return this; } } private class FormatSpecifier implements FormatString { private class BigDecimalLayout { private boolean dot = false; private StringBuilder exp; private StringBuilder mant; private int scale; public BigDecimalLayout(BigInteger intVal, int scale, BigDecimalLayoutForm form) { layout(intVal, scale, form); } // The exponent will be formatted as a sign ('+' or '-') followed // by the exponent zero-padded to include at least two digits. public char[] exponent() { return toCharArray(exp); } public boolean hasDot() { return dot; } public char[] mantissa() { return toCharArray(mant); } public int scale() { return scale; } private void layout(BigInteger intVal, int scale, BigDecimalLayoutForm form) { char[] coeff = intVal.toString().toCharArray(); this.scale = scale; // Construct a buffer, with sufficient capacity for all cases. // If E-notation is needed, length will be: +1 if negative, +1 // if '.' needed, +2 for "E+", + up to 10 for adjusted // exponent. Otherwise it could have +1 if negative, plus // leading "0.00000" mant = new StringBuilder(coeff.length + 14); if (scale == 0) { int len = coeff.length; if (len > 1) { mant.append(coeff[0]); if (form == BigDecimalLayoutForm.SCIENTIFIC) { mant.append('.'); dot = true; mant.append(coeff, 1, len - 1); exp = new StringBuilder("+"); if (len < 10) { exp.append("0").append(len - 1); } else { exp.append(len - 1); } } else { mant.append(coeff, 1, len - 1); } } else { mant.append(coeff); if (form == BigDecimalLayoutForm.SCIENTIFIC) { exp = new StringBuilder("+00"); } } return; } long adjusted = -(long) scale + (coeff.length - 1); if (form == BigDecimalLayoutForm.DECIMAL_FLOAT) { // count of padding zeros int pad = scale - coeff.length; if (pad >= 0) { // 0.xxx form mant.append("0."); dot = true; for (; pad > 0; pad--) { mant.append('0'); } mant.append(coeff); } else { if (-pad < coeff.length) { // xx.xx form mant.append(coeff, 0, -pad); mant.append('.'); dot = true; mant.append(coeff, -pad, scale); } else { // xx form mant.append(coeff, 0, coeff.length); for (int i = 0; i < -scale; i++) { mant.append('0'); } this.scale = 0; } } } else { // x.xxx form mant.append(coeff[0]); if (coeff.length > 1) { mant.append('.'); dot = true; mant.append(coeff, 1, coeff.length - 1); } exp = new StringBuilder(); if (adjusted != 0) { long abs = Math.abs(adjusted); // require sign exp.append(adjusted < 0 ? '-' : '+'); if (abs < 10) { exp.append('0'); } exp.append(abs); } else { exp.append("+00"); } } } private char[] toCharArray(StringBuilder sb) { if (sb == null) { return null; } char[] result = new char[sb.length()]; sb.getChars(0, result.length, result, 0); return result; } } private char c; private boolean dt = false; private Flags f = Flags.NONE; private int index = -1; // cache the line separator private final String ls = "\n"; private int precision; private int width; FormatSpecifier(FormatterJava formatter, String[] sa) { int idx = 0; index(sa[idx++]); flags(sa[idx++]); width(sa[idx++]); precision(sa[idx++]); String dtStr = sa[idx]; if (dtStr != null && !dtStr.isEmpty()) { dt = true; if (dtStr.equals("T")) { f.add(Flags.UPPERCASE); } } conversion(sa[++idx]); if (dt) { checkDateTime(); } else if (Conversion.isGeneral(c)) { checkGeneral(); } else if (Conversion.isCharacter(c)) { checkCharacter(); } else if (Conversion.isInteger(c)) { checkInteger(); } else if (Conversion.isFloat(c)) { checkFloat(); } else if (Conversion.isText(c)) { checkText(); } else { throw new IllegalArgumentException(String.valueOf(c)); } } @Override public int index() { return index; } @Override public void print(Object arg) throws IOException { if (dt) { printDateTime(arg); return; } switch (c) { case Conversion.DECIMAL_INTEGER: case Conversion.OCTAL_INTEGER: case Conversion.HEXADECIMAL_INTEGER: printInteger(arg); break; case Conversion.SCIENTIFIC: case Conversion.GENERAL: case Conversion.DECIMAL_FLOAT: case Conversion.HEXADECIMAL_FLOAT: printFloat(arg); break; case Conversion.CHARACTER: case Conversion.CHARACTER_UPPER: printCharacter(arg); break; case Conversion.BOOLEAN: printBoolean(arg); break; case Conversion.STRING: printString(arg); break; case Conversion.HASHCODE: printHashCode(arg); break; case Conversion.LINE_SEPARATOR: a.append(ls); break; case Conversion.PERCENT_SIGN: a.append('%'); break; default: assert false; } } @Override public String toString() { StringBuilder sb = new StringBuilder('%'); // Flags.UPPERCASE is set internally for legal conversions. Flags dupf = f.dup().remove(Flags.UPPERCASE); sb.append(dupf.toString()); if (index > 0) { sb.append(index).append('$'); } if (width != -1) { sb.append(width); } if (precision != -1) { sb.append('.').append(precision); } if (dt) { sb.append(f.contains(Flags.UPPERCASE) ? 'T' : 't'); } sb.append(f.contains(Flags.UPPERCASE) ? Character.toUpperCase(c) : c); return sb.toString(); } // Add a '.' to th mantissa if required private char[] addDot(char[] mant) { char[] tmp = mant; tmp = new char[mant.length + 1]; System.arraycopy(mant, 0, tmp, 0, mant.length); tmp[tmp.length - 1] = '.'; return tmp; } private int adjustWidth(int width, Flags f, boolean neg) { int newW = width; if (newW != -1 && neg && f.contains(Flags.PARENTHESES)) { newW--; } return newW; } private void checkBadFlags(Flags... badFlags) { for (int i = 0; i < badFlags.length; i++) { if (f.contains(badFlags[i])) { failMismatch(badFlags[i], c); } } } private void checkCharacter() { if (precision != -1) { throw new IllegalArgumentException("invalid pecision " + precision); } checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE, Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES); // '-' requires a width if (width == -1 && f.contains(Flags.LEFT_JUSTIFY)) { throw new IllegalArgumentException(toString()); } } private void checkDateTime() { if (precision != -1) { throw new IllegalArgumentException("invalid pecision " + precision); } checkBadFlags(Flags.ALTERNATE, Flags.PLUS, Flags.LEADING_SPACE, Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES); // '-' requires a width if (width == -1 && f.contains(Flags.LEFT_JUSTIFY)) { throw new IllegalArgumentException(toString()); } } private void checkFloat() { checkNumeric(); if (c == Conversion.HEXADECIMAL_FLOAT) { checkBadFlags(Flags.PARENTHESES, Flags.GROUP); } else if (c == Conversion.SCIENTIFIC) { checkBadFlags(Flags.GROUP); } else if (c == Conversion.GENERAL) { checkBadFlags(Flags.ALTERNATE); } } private void checkGeneral() { if ((c == Conversion.BOOLEAN || c == Conversion.HASHCODE) && f.contains(Flags.ALTERNATE)) { failMismatch(Flags.ALTERNATE, c); } // '-' requires a width if (width == -1 && f.contains(Flags.LEFT_JUSTIFY)) { throw new IllegalArgumentException(toString()); } checkBadFlags(Flags.PLUS, Flags.LEADING_SPACE, Flags.ZERO_PAD, Flags.GROUP, Flags.PARENTHESES); } private void checkInteger() { checkNumeric(); if (precision != -1) { throw new IllegalArgumentException("invalid pecision " + precision); } if (c == Conversion.DECIMAL_INTEGER) { checkBadFlags(Flags.ALTERNATE); } else if (c == Conversion.OCTAL_INTEGER) { checkBadFlags(Flags.GROUP); } else { checkBadFlags(Flags.GROUP); } } private void checkNumeric() { if (width != -1 && width < 0) { throw new IllegalArgumentException("illegal width " + width); } if (precision != -1 && precision < 0) { throw new IllegalArgumentException("invalid precision " + precision); } // '-' and '0' require a width if (width == -1 && (f.contains(Flags.LEFT_JUSTIFY) || f.contains(Flags.ZERO_PAD))) { throw new IllegalArgumentException(toString()); } // bad combination if ((f.contains(Flags.PLUS) && f.contains(Flags.LEADING_SPACE)) || (f.contains(Flags.LEFT_JUSTIFY) && f.contains(Flags.ZERO_PAD))) { throw new IllegalArgumentException(f.toString()); } } private void checkText() { if (precision != -1) { throw new IllegalArgumentException("invalid precision " + precision); } switch (c) { case Conversion.PERCENT_SIGN: if (f.valueOf() != Flags.LEFT_JUSTIFY.valueOf() && f.valueOf() != Flags.NONE.valueOf()) { throw new IllegalArgumentException(f.toString()); } // '-' requires a width if (width == -1 && f.contains(Flags.LEFT_JUSTIFY)) { throw new IllegalArgumentException(toString()); } break; case Conversion.LINE_SEPARATOR: if (width != -1) { throw new IllegalArgumentException(" illegal width: " + width); } if (f.valueOf() != Flags.NONE.valueOf()) { throw new IllegalArgumentException(f.toString()); } break; default: assert false; } } private char conversion(String s) { c = s.charAt(0); if (!dt) { if (!Conversion.isValid(c)) { throw new IllegalArgumentException(String.valueOf(c)); } if (Character.isUpperCase(c)) { f.add(Flags.UPPERCASE); } c = Character.toLowerCase(c); if (Conversion.isText(c)) { index = -2; } } return c; } private void failConversion(char c, Object arg) { throw new IllegalArgumentException("Conversion failed for '" + c + "' with class " + arg.getClass().getName()); } private void failMismatch(Flags f, char c) { String fs = f.toString(); throw new IllegalArgumentException("Match failed" + fs + " for char: " + c); } private Flags flags(String s) { f = Flags.parse(s); if (f.contains(Flags.PREVIOUS)) { index = -1; } return f; } private char getZero() { return zero; } private int index(String s) { if (s != null && !s.isEmpty()) { try { index = Integer.parseInt(s.substring(0, s.length() - 1)); } catch (NumberFormatException x) { assert false; } } else { index = 0; } return index; } private String justify(String s) { if (width == -1) { return s; } StringBuilder sb = new StringBuilder(); boolean pad = f.contains(Flags.LEFT_JUSTIFY); int sp = width - s.length(); if (!pad) { for (int i = 0; i < sp; i++) { sb.append(' '); } } sb.append(s); if (pad) { for (int i = 0; i < sp; i++) { sb.append(' '); } } return sb.toString(); } // neg := val < 0 private StringBuilder leadingSign(StringBuilder sb, boolean neg) { if (!neg) { if (f.contains(Flags.PLUS)) { sb.append('+'); } else if (f.contains(Flags.LEADING_SPACE)) { sb.append(' '); } } else { if (f.contains(Flags.PARENTHESES)) { sb.append('('); } else { sb.append('-'); } } return sb; } private StringBuilder localizedMagnitude(StringBuilder sb, char[] value, Flags f, int width) { if (sb == null) { sb = new StringBuilder(); } int begin = sb.length(); char zero = getZero(); // determine localized grouping separator and size char grpSep = '\0'; int grpSize = -1; char decSep = '\0'; int len = value.length; int dot = len; for (int j = 0; j < len; j++) { if (value[j] == '.') { dot = j; break; } } if (dot < len) { decSep = '.'; } if (f.contains(Flags.GROUP)) { grpSep = ','; grpSize = 3; } // localize the digits inserting group separators as necessary for (int j = 0; j < len; j++) { if (j == dot) { sb.append(decSep); // no more group separators after the decimal separator grpSep = '\0'; continue; } char c = value[j]; sb.append((char) ((c - '0') + zero)); if (grpSep != '\0' && j != dot - 1 && ((dot - j) % grpSize == 1)) { sb.append(grpSep); } } // apply zero padding len = sb.length(); if (width != -1 && f.contains(Flags.ZERO_PAD)) { for (int k = 0; k < width - len; k++) { sb.insert(begin, zero); } } return sb; } private StringBuilder localizedMagnitude(StringBuilder sb, long value, Flags f, int width) { char[] va = Long.toString(value, 10).toCharArray(); return localizedMagnitude(sb, va, f, width); } private int precision(String s) { precision = -1; if (s != null && !s.isEmpty()) { try { // remove the '.' precision = Integer.parseInt(s.substring(1)); if (precision < 0) { throw new IllegalArgumentException("precision: " + precision); } } catch (NumberFormatException x) { assert false; } } return precision; } private void print(BigDecimal value) throws IOException { if (c == Conversion.HEXADECIMAL_FLOAT) { failConversion(c, value); } StringBuilder sb = new StringBuilder(); boolean neg = value.signum() == -1; BigDecimal v = value.abs(); // leading sign indicator leadingSign(sb, neg); // the value print(sb, v, f, c, precision, neg); // trailing sign indicator trailingSign(sb, neg); // justify based on width a.append(justify(sb.toString())); } private void print(BigInteger value) throws IOException { StringBuilder sb = new StringBuilder(); boolean neg = value.signum() == -1; BigInteger v = value.abs(); // leading sign indicator leadingSign(sb, neg); // the value if (c == Conversion.DECIMAL_INTEGER) { char[] va = v.toString().toCharArray(); localizedMagnitude(sb, va, f, adjustWidth(width, f, neg)); } else if (c == Conversion.OCTAL_INTEGER) { String s = v.toString(8); int len = s.length() + sb.length(); if (neg && f.contains(Flags.PARENTHESES)) { len++; } // apply ALTERNATE (radix indicator for octal) before ZERO_PAD if (f.contains(Flags.ALTERNATE)) { len++; sb.append('0'); } if (f.contains(Flags.ZERO_PAD)) { for (int i = 0; i < width - len; i++) { sb.append('0'); } } sb.append(s); } else if (c == Conversion.HEXADECIMAL_INTEGER) { String s = v.toString(16); int len = s.length() + sb.length(); if (neg && f.contains(Flags.PARENTHESES)) { len++; } // apply ALTERNATE (radix indicator for hex) before ZERO_PAD if (f.contains(Flags.ALTERNATE)) { len += 2; sb.append(f.contains(Flags.UPPERCASE) ? "0X" : "0x"); } if (f.contains(Flags.ZERO_PAD)) { for (int i = 0; i < width - len; i++) { sb.append('0'); } } if (f.contains(Flags.UPPERCASE)) { s = s.toUpperCase(); } sb.append(s); } // trailing sign indicator trailingSign(sb, value.signum() == -1); // justify based on width a.append(justify(sb.toString())); } private void print(byte value) throws IOException { long v = value; if (value < 0 && (c == Conversion.OCTAL_INTEGER || c == Conversion.HEXADECIMAL_INTEGER)) { v += 1L << 8; assert v >= 0 : v; } print(v); } private void print(Date t, char c) throws IOException { StringBuilder sb = new StringBuilder(); print(sb, t, c); // justify based on width String s = justify(sb.toString()); if (f.contains(Flags.UPPERCASE)) { s = s.toUpperCase(); } a.append(s); } private void print(double value) throws IOException { StringBuilder sb = new StringBuilder(); boolean neg = Double.compare(value, 0.0) == -1; if (!Double.isNaN(value)) { double v = Math.abs(value); // leading sign indicator leadingSign(sb, neg); // the value if (!Double.isInfinite(v)) { print(sb, v, f, c, precision, neg); } else { sb.append(f.contains(Flags.UPPERCASE) ? "INFINITY" : "Infinity"); } // trailing sign indicator trailingSign(sb, neg); } else { sb.append(f.contains(Flags.UPPERCASE) ? "NAN" : "NaN"); } // justify based on width a.append(justify(sb.toString())); } private void print(float value) throws IOException { print((double) value); } private void print(int value) throws IOException { long v = value; if (value < 0 && (c == Conversion.OCTAL_INTEGER || c == Conversion.HEXADECIMAL_INTEGER)) { v += 1L << 32; assert v >= 0 : v; } print(v); } private void print(long value) throws IOException { StringBuilder sb = new StringBuilder(); if (c == Conversion.DECIMAL_INTEGER) { boolean neg = value < 0; char[] va; if (value < 0) { va = Long.toString(value, 10).substring(1).toCharArray(); } else { va = Long.toString(value, 10).toCharArray(); } // leading sign indicator leadingSign(sb, neg); // the value localizedMagnitude(sb, va, f, adjustWidth(width, f, neg)); // trailing sign indicator trailingSign(sb, neg); } else if (c == Conversion.OCTAL_INTEGER) { checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE, Flags.PLUS); String s = Long.toOctalString(value); int len = f.contains(Flags.ALTERNATE) ? s.length() + 1 : s.length(); // apply ALTERNATE (radix indicator for octal) before ZERO_PAD if (f.contains(Flags.ALTERNATE)) { sb.append('0'); } if (f.contains(Flags.ZERO_PAD)) { for (int i = 0; i < width - len; i++) { sb.append('0'); } } sb.append(s); } else if (c == Conversion.HEXADECIMAL_INTEGER) { checkBadFlags(Flags.PARENTHESES, Flags.LEADING_SPACE, Flags.PLUS); String s = Long.toHexString(value); int len = f.contains(Flags.ALTERNATE) ? s.length() + 2 : s.length(); // apply ALTERNATE (radix indicator for hex) before ZERO_PAD if (f.contains(Flags.ALTERNATE)) { sb.append(f.contains(Flags.UPPERCASE) ? "0X" : "0x"); } if (f.contains(Flags.ZERO_PAD)) { for (int i = 0; i < width - len; i++) { sb.append('0'); } } if (f.contains(Flags.UPPERCASE)) { s = s.toUpperCase(); } sb.append(s); } // justify based on width a.append(justify(sb.toString())); } private void print(short value) throws IOException { long v = value; if (value < 0 && (c == Conversion.OCTAL_INTEGER || c == Conversion.HEXADECIMAL_INTEGER)) { v += 1L << 16; assert v >= 0 : v; } print(v); } private void print(String s) throws IOException { if (precision != -1 && precision < s.length()) { s = s.substring(0, precision); } if (f.contains(Flags.UPPERCASE)) { s = s.toUpperCase(); } a.append(justify(s)); } // value > 0 private void print(StringBuilder sb, BigDecimal value, Flags f, char c, int precision, boolean neg) throws IOException { if (c == Conversion.SCIENTIFIC) { // Create a new BigDecimal with the desired precision. int prec = precision == -1 ? 6 : precision; int scale = value.scale(); int origPrec = value.precision(); int nzeros = 0; int compPrec; if (prec > origPrec - 1) { compPrec = origPrec; nzeros = prec - (origPrec - 1); } else { compPrec = prec + 1; } MathContext mc = new MathContext(compPrec); BigDecimal v = new BigDecimal(value.unscaledValue(), scale, mc); BigDecimalLayout bdl = new BigDecimalLayout(v.unscaledValue(), v.scale(), BigDecimalLayoutForm.SCIENTIFIC); char[] mant = bdl.mantissa(); // Add a decimal point if necessary. The mantissa may not // contain a decimal point if the scale is zero (the internal // representation has no fractional part) or the original // precision is one. Append a decimal point if '#' is set or if // we require zero padding to get to the requested precision. if ((origPrec == 1 || !bdl.hasDot()) && (nzeros > 0 || f.contains(Flags.ALTERNATE))) { mant = addDot(mant); } // Add trailing zeros in the case precision is greater than // the number of available digits after the decimal separator. mant = trailingZeros(mant, nzeros); char[] exp = bdl.exponent(); int newW = width; if (width != -1) { newW = adjustWidth(width - exp.length - 1, f, neg); } localizedMagnitude(sb, mant, f, newW); sb.append(f.contains(Flags.UPPERCASE) ? 'E' : 'e'); Flags flags = f.dup().remove(Flags.GROUP); char sign = exp[0]; assert sign == '+' || sign == '-'; sb.append(exp[0]); char[] tmp = new char[exp.length - 1]; System.arraycopy(exp, 1, tmp, 0, exp.length - 1); sb.append(localizedMagnitude(null, tmp, flags, -1)); } else if (c == Conversion.DECIMAL_FLOAT) { // Create a new BigDecimal with the desired precision. int prec = precision == -1 ? 6 : precision; int scale = value.scale(); if (scale > prec) { // more "scale" digits than the requested "precision int compPrec = value.precision(); if (compPrec <= scale) { // case of 0.xxxxxx value = value.setScale(prec, RoundingMode.HALF_UP); } else { compPrec -= scale - prec; value = new BigDecimal(value.unscaledValue(), scale, new MathContext(compPrec)); } } BigDecimalLayout bdl = new BigDecimalLayout( value.unscaledValue(), value.scale(), BigDecimalLayoutForm.DECIMAL_FLOAT); char[] mant = bdl.mantissa(); int nzeros = bdl.scale() < prec ? prec - bdl.scale() : 0; // Add a decimal point if necessary. The mantissa may not // contain a decimal point if the scale is zero (the internal // representation has no fractional part). Append a decimal // point if '#' is set or we require zero padding to get to the // requested precision. if (bdl.scale() == 0 && (f.contains(Flags.ALTERNATE) || nzeros > 0)) { mant = addDot(bdl.mantissa()); } // Add trailing zeros if the precision is greater than the // number of available digits after the decimal separator. mant = trailingZeros(mant, nzeros); localizedMagnitude(sb, mant, f, adjustWidth(width, f, neg)); } else if (c == Conversion.GENERAL) { int prec = precision; if (precision == -1) { prec = 6; } else if (precision == 0) { prec = 1; } BigDecimal tenToTheNegFour = BigDecimal.valueOf(1, 4); BigDecimal tenToThePrec = BigDecimal.valueOf(1, -prec); if (value.equals(BigDecimal.ZERO) || ((value.compareTo(tenToTheNegFour) != -1) && (value.compareTo(tenToThePrec) == -1))) { int e = -value.scale() + (value.unscaledValue().toString().length() - 1); // xxx.yyy // g precision (# sig digits) = #x + #y // f precision = #y // exponent = #x - 1 // => f precision = g precision - exponent - 1 // 0.000zzz // g precision (# sig digits) = #z // f precision = #0 (after '.') + #z // exponent = - #0 (after '.') - 1 // => f precision = g precision - exponent - 1 prec = prec - e - 1; print(sb, value, f, Conversion.DECIMAL_FLOAT, prec, neg); } else { print(sb, value, f, Conversion.SCIENTIFIC, prec - 1, neg); } } else if (c == Conversion.HEXADECIMAL_FLOAT) { // This conversion isn't supported. The error should be // reported earlier. assert false; } } private Appendable print(StringBuilder sb, Date t, char c) throws IOException { assert width == -1; if (sb == null) { sb = new StringBuilder(); } int i; Flags flags; char sep; switch (c) { case DateTime.HOUR_OF_DAY_0: // 'H' (00 - 23) case DateTime.HOUR_0: // 'I' (01 - 12) case DateTime.HOUR_OF_DAY: // 'k' (0 - 23) -- like H case DateTime.HOUR: // 'l' (1 - 12) -- like I i = t.getHours(); if (c == DateTime.HOUR_0 || c == DateTime.HOUR) { i = i == 0 || i == 12 ? 12 : i % 12; } flags = c == DateTime.HOUR_OF_DAY_0 || c == DateTime.HOUR_0 ? Flags.ZERO_PAD : Flags.NONE; sb.append(localizedMagnitude(null, i, flags, 2)); break; case DateTime.MINUTE: // 'M' (00 - 59) i = t.getMinutes(); flags = Flags.ZERO_PAD; sb.append(localizedMagnitude(null, i, flags, 2)); break; case DateTime.SECONDS_SINCE_EPOCH: // 's' (0 - 99...?) flags = Flags.NONE; sb.append(localizedMagnitude(null, t.getTime() / 1000, flags, width)); break; case DateTime.SECOND: // 'S' (00 - 60 - leap second) i = t.getSeconds(); flags = Flags.ZERO_PAD; sb.append(localizedMagnitude(null, i, flags, 2)); break; case DateTime.ZONE_NUMERIC: // 'z' ({-|+}####) - ls minus? i = t.getTimezoneOffset(); boolean neg = i < 0; sb.append(neg ? '-' : '+'); if (neg) { i = -i; } int min = i / 60000; // combine minute and hour into a single integer int offset = (min / 60) * 100 + (min % 60); flags = Flags.ZERO_PAD; sb.append(localizedMagnitude(null, offset, flags, 4)); break; case DateTime.CENTURY: // 'C' (00 - 99) case DateTime.YEAR_2: // 'y' (00 - 99) case DateTime.YEAR_4: // 'Y' (0000 - 9999) i = t.getYear(); int size = 2; switch (c) { case DateTime.CENTURY: i /= 100; break; case DateTime.YEAR_2: i %= 100; break; case DateTime.YEAR_4: size = 4; break; } flags = Flags.ZERO_PAD; sb.append(localizedMagnitude(null, i, flags, size)); break; case DateTime.DAY_OF_MONTH_0: // 'd' (01 - 31) case DateTime.DAY_OF_MONTH: // 'e' (1 - 31) -- like d i = t.getDate(); flags = c == DateTime.DAY_OF_MONTH_0 ? Flags.ZERO_PAD : Flags.NONE; sb.append(localizedMagnitude(null, i, flags, 2)); break; case DateTime.MONTH: // 'm' (01 - 12) i = t.getMonth(); flags = Flags.ZERO_PAD; sb.append(localizedMagnitude(null, i, flags, 2)); break; // Composites case DateTime.TIME: // 'T' (24 hour hh:mm:ss - %tH:%tM:%tS) case DateTime.TIME_24_HOUR: // 'R' (hh:mm same as %H:%M) sep = ':'; print(sb, t, DateTime.HOUR_OF_DAY_0).append(sep); print(sb, t, DateTime.MINUTE); if (c == DateTime.TIME) { sb.append(sep); print(sb, t, DateTime.SECOND); } break; case DateTime.TIME_12_HOUR: // 'r' (hh:mm:ss [AP]M) sep = ':'; print(sb, t, DateTime.HOUR_0).append(sep); print(sb, t, DateTime.MINUTE).append(sep); print(sb, t, DateTime.SECOND).append(' '); // this may be in wrong place for some locales StringBuilder tsb = new StringBuilder(); print(tsb, t, DateTime.AM_PM); sb.append(tsb.toString().toUpperCase()); break; case DateTime.DATE_TIME: // 'c' (Sat Nov 04 12:02:33 EST 1999) sep = ' '; print(sb, t, DateTime.NAME_OF_DAY_ABBREV).append(sep); print(sb, t, DateTime.NAME_OF_MONTH_ABBREV).append(sep); print(sb, t, DateTime.DAY_OF_MONTH_0).append(sep); print(sb, t, DateTime.TIME).append(sep); print(sb, t, DateTime.ZONE).append(sep); print(sb, t, DateTime.YEAR_4); break; case DateTime.DATE: // 'D' (mm/dd/yy) sep = '/'; print(sb, t, DateTime.MONTH).append(sep); print(sb, t, DateTime.DAY_OF_MONTH_0).append(sep); print(sb, t, DateTime.YEAR_2); break; case DateTime.ISO_STANDARD_DATE: // 'F' (%Y-%m-%d) sep = '-'; print(sb, t, DateTime.YEAR_4).append(sep); print(sb, t, DateTime.MONTH).append(sep); print(sb, t, DateTime.DAY_OF_MONTH_0); break; default: throw new IllegalArgumentException("Format flag: '" + c + "' is not supported"); } return sb; } // !Double.isInfinite(value) && !Double.isNaN(value) private void print(StringBuilder sb, double value, Flags f, char c, int precision, boolean neg) throws IOException { sb.append(value); } private void printBoolean(Object arg) throws IOException { String s; if (arg != null) { s = arg instanceof Boolean ? arg.toString() : Boolean.toString(true); } else { s = Boolean.toString(false); } print(s); } private void printCharacter(Object arg) throws IOException { if (arg == null) { print("null"); return; } String s = null; if (arg instanceof Character) { s = arg.toString(); } else if (arg instanceof Byte) { byte i = ((Byte) arg).byteValue(); if (Character.isValidCodePoint(i)) { s = new String(Character.toChars(i)); } else { throw new IllegalArgumentException("invalid code point " + i); } } else if (arg instanceof Short) { short i = ((Short) arg).shortValue(); if (Character.isValidCodePoint(i)) { s = new String(Character.toChars(i)); } else { throw new IllegalArgumentException("invalid code point " + i); } } else if (arg instanceof Integer) { int i = ((Integer) arg).intValue(); if (Character.isValidCodePoint(i)) { s = new String(Character.toChars(i)); } else { throw new IllegalArgumentException("invalid code point " + i); } } else { failConversion(c, arg); } print(s); } private void printDateTime(Object arg) throws IOException { if (arg == null) { print("null"); return; } Date date = null; // Instead of Calendar.setLenient(true), perhaps we should // wrap the IllegalArgumentException that might be thrown? if (arg instanceof Long) { // Note that the following method uses an instance of the // default time zone (TimeZone.getDefaultRef(). date = new Date((Long) arg); } else if (arg instanceof Date) { // Note that the following method uses an instance of the // default time zone (TimeZone.getDefaultRef(). date = (Date) arg; } else { failConversion(c, arg); } print(date, c); } private void printFloat(Object arg) throws IOException { if (arg == null) { print("null"); } else if (arg instanceof Float) { print(((Float) arg).floatValue()); } else if (arg instanceof Double) { print(((Double) arg).doubleValue()); } else if (arg instanceof BigDecimal) { print((BigDecimal) arg); } else { failConversion(c, arg); } } private void printHashCode(Object arg) throws IOException { String s = arg == null ? "null" : Integer.toHexString(arg.hashCode()); print(s); } // -- Methods to support throwing exceptions -- private void printInteger(Object arg) throws IOException { if (arg == null) { print("null"); } else if (arg instanceof Byte) { print(((Byte) arg).byteValue()); } else if (arg instanceof Short) { print(((Short) arg).shortValue()); } else if (arg instanceof Integer) { print(((Integer) arg).intValue()); } else if (arg instanceof Long) { print(((Long) arg).longValue()); } else if (arg instanceof BigInteger) { print((BigInteger) arg); } else { failConversion(c, arg); } } private void printString(Object arg) throws IOException { if (arg == null) { print("null"); } else { print(arg.toString()); } } // neg := val < 0 private StringBuilder trailingSign(StringBuilder sb, boolean neg) { if (neg && f.contains(Flags.PARENTHESES)) { sb.append(')'); } return sb; } // Add trailing zeros in the case precision is greater than the number // of available digits after the decimal separator. private char[] trailingZeros(char[] mant, int nzeros) { char[] tmp = mant; if (nzeros > 0) { tmp = new char[mant.length + nzeros]; System.arraycopy(mant, 0, tmp, 0, mant.length); for (int i = mant.length; i < tmp.length; i++) { tmp[i] = '0'; } } return tmp; } private int width(String s) { width = -1; if (s != null && !s.isEmpty()) { try { width = Integer.parseInt(s); if (width < 0) { throw new IllegalArgumentException("Illegal width " + width); } } catch (NumberFormatException x) { assert false; } } return width; } } private interface FormatString { int index(); void print(Object arg) throws IOException; //@Override String toString(); } // %[argument_index$][flags][width][.precision][t]conversion private static final String formatSpecifier = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"; private static RegExp fsPattern = RegExp.compile(formatSpecifier); private Appendable a; private IOException lastException; private final char zero = '0'; public FormatterJava() { init(new StringBuilder()); } public FormatterJava(Appendable a) { if (a == null) { a = new StringBuilder(); } init(a); } public FormatterJava format(String format, Object... args) { // index of last argument referenced int last = -1; // last ordinary index int lasto = -1; FormatString[] fsa = parse(format); for (int i = 0; i < fsa.length; i++) { FormatString fs = fsa[i]; int index = fs.index(); try { switch (index) { case -2: // fixed string, "%n", or "%%" fs.print(null); break; case -1: // relative index if (last < 0 || (args != null && last > args.length - 1)) { throw new IllegalArgumentException(fs.toString()); } fs.print(args == null ? null : args[last]); break; case 0: // ordinary index lasto++; last = lasto; if (args != null && lasto > args.length - 1) { throw new IllegalArgumentException(fs.toString()); } fs.print(args == null ? null : args[lasto]); break; default: // explicit index last = index - 1; if (args != null && last > args.length - 1) { throw new IllegalArgumentException(fs.toString()); } fs.print(args == null ? null : args[last]); break; } } catch (IOException x) { lastException = x; } } return this; } public IOException ioException() { return lastException; } public Appendable out() { return a; }; @Override public String toString() { return a.toString(); } private void checkText(String s) { int idx; // If there are any '%' in the given string, we got a bad format // specifier. if ((idx = s.indexOf('%')) != -1) { char c = idx > s.length() - 2 ? '%' : s.charAt(idx + 1); throw new IllegalArgumentException(String.valueOf(c)); } } // Initialize internal data. private void init(Appendable a) { this.a = a; } // Look for format specifiers in the format string. private FormatString[] parse(String s) { ArrayList<FormatString> al = new ArrayList<>(); while (s.length() > 0) { MatchResult m = fsPattern.exec(s); if (m != null) { int i = m.getIndex(); if (i > 0) { // Anything between the start of the string and the beginning // of the format specifier is either fixed text or contains // an invalid format string. String staticText = s.substring(0, i); // Make sure we didn't miss any invalid format specifiers checkText(staticText); // Assume previous characters were fixed text al.add(new FixedString(staticText)); } // Expect 6 groups in regular expression String[] sa = new String[6]; for (int j = 1; j < m.getGroupCount(); j++) { sa[j - 1] = m.getGroup(j); // System.out.print(sa[j] + " "); } // System.out.println(); al.add(new FormatSpecifier(this, sa)); // trim parsed string s = s.substring(i + m.getGroup(0).length(), s.length()); } else { // No more valid format specifiers. Check for possible invalid // format specifiers. checkText(s); // The rest of the string is fixed text al.add(new FixedString(s)); break; } } // FormatString[] fs = new FormatString[al.size()]; // for (int j = 0; j < al.size(); j++) // System.out.println(((FormatString) al.get(j)).toString()); return al.toArray(new FormatString[0]); } }