diff --git a/classpath/avian/FormatString.java b/classpath/avian/FormatString.java new file mode 100644 index 0000000000..a66c3cfa9e --- /dev/null +++ b/classpath/avian/FormatString.java @@ -0,0 +1,992 @@ +/* Copyright (c) 2008-2015, Avian Contributors + + Permission to use, copy, modify, and/or distribute this software + for any purpose with or without fee is hereby granted, provided + that the above copyright notice and this permission notice appear + in all copies. + + There is NO WARRANTY for this software. See license.txt for + details. */ + +package avian; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.IllegalFormatException; +import java.util.List; + +// ------------------------------------------------------------------------- // +// things that must be done in order to call this semi-complete: +// ------------------------------------------------------------------------- // +// * get the date formatter working for individual fields at a minimum +// ------------------------------------------------------------------------- // + +/** + * A Java flavored printf for classpath-challenged JVMs. + * + * Each instance of this class is a threadsafe pre-parsed format pattern. Refer + * to the very detailed class description of java.util.Formatter in the OpenJDK + * API documentation for an explanation of the supported formats. + * + * Should be easily portable to other Java runtimes that do not include the + * printf functionality that was introduced in Java 5. + * + * Aims to be lightweight and reasonably fast, and provide reasonably complete + * API compatibility with the OpenJDK implementation. To clarify what + * "reasonably complete" means in this context, this implementation should + * accept any valid format string that the OpenJDK version accepts. However, + * it should not be relied upon to throw an Exception for every format pattern + * that the OpenJDK implementation does. + * + * If your program's behavior relies on the side effects from an Exception + * being thrown for an invalid format string, this might not be for you. + * + * Perhaps more troubling is the fact that the correct localization of numbers + * and temporal values is barely even attempted, even though the parser accepts + * the flags without even a warning. However, now you have been warned. + * + * @author bcg + */ +public final class FormatString { + + /** Parses a format string and returns a compiled representation of it. */ + public static final FormatString compile(String fmt) { + return new FormatString(fmt); + } + + /** The original string value that was parsed */ + public String source() { + return _source; + } + + /** Processes the supplied arguments through the compiled format string + and returns the result as a String. */ + public final String format(Object... args) { + final StringBuilder bldr = new StringBuilder(); + try { + format(bldr, args); + return bldr.toString(); + } catch (IOException e) { + throw new IllegalStateException( + "Should not get IOException when writing to StringBuilder", e + ); + } + } + + /** Processes the supplied arguments through the compiled format string + and writes the result of each component directly to an Appendable */ + public final void format(final Appendable a, Object... fmt_args) + throws IOException { + final Object[] args = fmt_args != null ? fmt_args : new Object[0]; + int cntr = 0; + for (final FmtCmpnt cmp : _components) { + if (cmp._conversion == CONV_LITRL) { + a.append(cmp._source); + continue; + } + final Object arg; + if (!acceptsArgument(cmp._conversion)) { + arg = null; + } else { + final int index = cmp._argument_index; + switch (index) { + case AIDX_NONE: + if ((cntr) >= args.length) { + throw new IllegalFormatException( + "Format specified at least " + (cntr+1) + + " arguments, but " + cntr + " were supplied." + ); + } + arg = args[cntr++]; + break; + case AIDX_PREV: + arg = args[cntr]; + break; + default: + if (index < 1) { + throw new IllegalArgumentException(); + } else if (index > args.length) { + throw new IllegalArgumentException(); + } else { + arg = args[index - 1]; + } + } + } + convert(a, arg, cmp._conversion, cmp._flags, cmp._width, cmp._precision); + } + } + + //- conversions + static final byte CONV_LITRL = 0x0; + static final byte CONV_NLINE = 0x1; + static final byte CONV_PRCNT = 0x2; + static final byte CONV_BOOLN = 0x3; + static final byte CONV_DTIME = 0x4; + static final byte CONV_STRNG = 0x5; + static final byte CONV_HCODE = 0x6; + static final byte CONV_CHRCT = 0x7; + static final byte CONV_DECML = 0x8; + static final byte CONV_OCTAL = 0x9; + static final byte CONV_HXDEC = 0xA; + static final byte CONV_CPSCI = 0xB; + static final byte CONV_GNSCI = 0xC; + static final byte CONV_FLOAT = 0xD; + static final byte CONV_HXEXP = 0xE; + + //- format component flags + static final byte FLAG_FORCE_UPPER_CASE = (byte)(1<<7); + static final byte FLAG_NEGATIVES_IN_PARENS = (byte)(1<<6); // ('(') + static final byte FLAG_GROUPING_SEPARATORS = (byte)(1<<5); // (',') + static final byte FLAG_LEADING_ZERO_PADDED = (byte)(1<<4); // ('0') + static final byte FLAG_LEADING_SPACE_PADDED = (byte)(1<<3); // (' ') + static final byte FLAG_ALWAYS_INCLUDES_SIGN = (byte)(1<<2); // ('+') + static final byte FLAG_ALTERNATE_FORM = (byte)(1<<1); // ('#') + static final byte FLAG_LEFT_JUSTIFIED = (byte)(1<<0); // ('-') + + //- conversion capability flags + static final byte CFLG_WDTH_SUPPRT = CONV_PRCNT; + static final byte CFLG_ACCEPTS_ARG = CONV_BOOLN; + static final byte CFLG_NUMERIC_VAL = CONV_DECML; + static final byte CFLG_PREC_SUPPRT = CONV_STRNG; + + //- special argument indices + static final int AIDX_PREV = -1; + static final int AIDX_NONE = 0; + + /** the original serialized format string */ + private final String _source; + + /** array of components parsed from the source string */ + private final FmtCmpnt[] _components; + + /*/ keeping this private for now to encourage access through the static + compile method, which might allow caching format string instances if it + turns out there is an advantage to that. /*/ + /** Constructor */ + private FormatString(final String fmt) { + this._source = fmt; + final List cmps = new ArrayList(); + for ( int i = 0; (i = next(fmt, cmps, i)) > -1; ); + this._components = cmps.toArray(new FmtCmpnt[cmps.size()]); + } + + /** Iterates over the tokens in an input string to extract the components */ + private static final int next( + final String fmt, final List cmps, final int startIndex) { + final int strln = fmt.length(); + if (startIndex >= strln) { + return -1; + } + final char c = fmt.charAt(startIndex); + if (c == '%') { + // this is the start of a specifier + final FmtSpecBldr bldr = new FmtSpecBldr(); + for (int i = startIndex + 1; i < strln; i++) { + final char ch = fmt.charAt(i); + final FmtCmpnt cmp = bldr.append(ch); + if (cmp != null) { + cmps.add(cmp); + return (i+1); + } + } + throw new IllegalFormatException("Incomplete specifier at end of fmt"); + } else { + // this is the start of a literal + final StringBuilder literal = new StringBuilder(); + literal.append(c); + for (int i = startIndex + 1; i < strln; i++) { + final char ch = fmt.charAt(i); + // write the current buffer if the next character starts a specifier + if (ch == '%') { + final FmtCmpnt cmp = new FmtCmpnt(literal.toString()); + cmps.add(cmp); + return i; + } + literal.append(ch); + } + // write the current buffer if the end of the format has been reached + final FmtCmpnt cmp = new FmtCmpnt(literal.toString()); + cmps.add(cmp); + return -1; + } + } + + /** Checks a flag byte to see if a given flag is set. Only FLAG_* constants + from the enclosing class should be passed in for toCheck... otherwise the + behavior is undefined. */ + static final boolean checkFlag(final byte flags, final byte toCheck) { + return (flags & toCheck) != 0; + } + + /** Checks if a given conversion accepts(requires) an argument. Only the CONV_ + flags from the enclosing class should be passed in, otherwise the result + of this method is undefined as should not be used. */ + static final boolean acceptsArgument(final byte conversion) { + return conversion >= CFLG_ACCEPTS_ARG; + } + + /** Checks if a given conversion allows specifying a precision. Only the CONV_ + flags from the enclosing class should be passed in, otherwise the result + of this method is undefined as should not be used. */ + static final boolean precisionSupported(final byte conversion) { + return conversion >= CFLG_PREC_SUPPRT; + } + + /** Checks if a given conversion allows specifying a width. Only the CONV_ + flags from the enclosing class should be passed in, otherwise the result + of this method is undefined and should not be trusted. */ + static final boolean widthSupported(final byte conversion) { + return conversion >= CFLG_WDTH_SUPPRT; + } + + /** Checks if a given conversion expects a numeric value. Only the CONV_ + flags from the enclosing class should be passed in, otherwise the result + of this method is undefined and should not be trusted. */ + static final boolean isNumeric(final byte conversion) { + return conversion >= CFLG_NUMERIC_VAL; + } + + /** The newline character for the current platform. */ + static final String NEWLINE = System.getProperty("line.separator"); + + /** Performs conversion on the supplied argument */ + static final void convert( + final Appendable appendable, + final Object arg, + final byte conversion, + final byte flags, + final int width, + final int precision) throws IOException { + int radix = 0; + switch (conversion) { + case CONV_LITRL: + throw new IllegalArgumentException("cannot convert a literal"); + case CONV_NLINE: + appendable.append(NEWLINE); + return; + case CONV_PRCNT: + convertPercent(appendable, arg, flags, width, precision); + return; + case CONV_BOOLN: + convertBoolean(appendable, arg, flags, width, precision); + return; + case CONV_DTIME: + convertDate(appendable, arg, flags, width, precision); + return; + case CONV_STRNG: + convertString(appendable, arg, flags, width, precision); + return; + case CONV_HCODE: + convertHashcode(appendable, arg, flags, width, precision); + return; + case CONV_CHRCT: + convertChar(appendable, arg, flags, width, precision); + return; + case CONV_DECML: if (radix == 0) { radix = 10; }; + case CONV_OCTAL: if (radix == 0) { radix = 8; }; + case CONV_HXDEC: if (radix == 0) { radix = 16; }; + if (arg instanceof Long) { + convertLong(appendable, (Long) arg, flags, width, precision, radix); + } else { + convertInteger(appendable, arg, flags, width, precision, radix); + } + return; + case CONV_CPSCI: + case CONV_GNSCI: + case CONV_FLOAT: + case CONV_HXEXP: + convertFloat(appendable, arg, flags, width, precision, 10); + return; + } + throw new IllegalStateException("not implemented: " + conversion); + } + + static void convertPercent( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val = "%"; + appendify(a, val, flags, width, precision); + } + + static void convertDate( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val = (arg == null) ? "null" : arg.toString(); + appendify(a, val, flags, width, precision); + } + + static void convertString( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val = (arg == null) ? "null" : arg.toString(); + appendify(a, val, flags, width, precision); + } + + static void convertHashcode( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val = (arg == null) + ? "null" + : Integer.toHexString(arg.hashCode()); + appendify(a, val, flags, width, precision); + } + + static void convertBoolean( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val; + if (arg == null) { + val = "false"; + } else if (arg instanceof Boolean) { + val = String.valueOf(arg); + } else { + val = "true"; + } + appendify(a, val, flags, width, precision); + } + + static void convertChar( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision) throws IOException { + final String val; + if (arg instanceof Character) { + val = ((Character) arg).toString(); + } else if ( arg instanceof Byte || + arg instanceof Short || + arg instanceof Integer ){ + final int codePoint = ((Number) arg).intValue(); + if (codePoint >= 0 && codePoint <= 0x10FFFF) { //<-- isValidCodePoint()? + val = new String(Character.toChars(codePoint)); + } else { + throw new IllegalFormatException("Invalid code point: " + arg); + } + } else { + throw new IllegalFormatException("Cannot do char conversion: " + arg); + } + appendify(a, val, flags, width, precision); + } + + // FIXME: this is broken for octal formats with negative values + static void convertLong( + final Appendable a, + final Long arg, + final byte flags, + final int width, + final int precision, + final int radix) throws IOException { + final String val; + final Long n = arg; + final long longValue = n.longValue(); + if (radix == 10 || longValue > -1) { + val = Long.toString(longValue, radix); + } else { + final long upper = 0xFFFFFFFFL&(longValue>>31); + final long lower = 0xFFFFFFFFL&(longValue); + val = Long.toString(upper, radix) + Long.toString(lower, radix); + } + appendify(a, val, flags, width, precision); + } + + static void convertInteger( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision, + final int radix) throws IOException { + final String val; + final Number n = (Number) arg; + final long longValue = n.longValue(); + final long modifier; + if (arg instanceof Integer) modifier = 0xFFFFFFFFL+1; else + if (arg instanceof Short) modifier = 0xFFFFL+1; else + if (arg instanceof Byte) modifier = 0xFFL+1; + else throw new IllegalFormatException( + "not an integer number: " + (arg != null ? arg.getClass() : null) + ); + if (radix != 10 && longValue < 0) { + val = Long.toString(longValue + modifier, radix); + } else { + val = Long.toString(longValue, radix); + } + appendify(a, val, flags, width, precision); + } + + // FIXME: I'm lazy, so hexidecimal exponential isn't implemented, sorry - bcg + static void convertFloat( + final Appendable a, + final Object arg, + final byte flags, + final int width, + final int precision, + final int radix) throws IOException { + final String val; + final Number n = (Number) arg; + if (arg instanceof Float) { + val = Float.toString(n.floatValue()); + } else if (arg instanceof Double) { + val = Double.toString(n.doubleValue()); + } else { + throw new IllegalFormatException( + "not a floating point number: " + (arg != null ? arg.getClass() : null) + ); + } + appendify(a, val, flags, width, precision); + } + + static void appendify( + final Appendable a, + final String val, + final byte flags, + final int width, + final int precision) throws IOException { + String result = val; + if (checkFlag(flags, FLAG_FORCE_UPPER_CASE)) { + result = result.toUpperCase(); + } + // TODO: implement other flags + // (+) always include sign + // (,) grouping separators + // (() negatives in parentheses + if (precision > 0) { + // FIXME: this behavior should be different for floating point numbers + final int difference = result.length() - precision; + if (difference > 0) { + result = result.substring(0, precision); + a.append(result); + return; + } + } + if (width > 0) { + final int difference = width - result.length(); + final boolean leftJustified = checkFlag(flags, FLAG_LEFT_JUSTIFIED); + if (!leftJustified && difference > 0) { + char fill = checkFlag(flags, FLAG_LEADING_ZERO_PADDED) ? '0' : ' '; + fill(a, difference, fill); + } + a.append(result); + if (leftJustified && difference > 0) { + fill(a, difference, ' '); + } + return; + } + a.append(result); + } + + private static void fill(Appendable a, int num, char c) throws IOException { + while (num > 0) { + a.append(c); + num--; + } + } + + /** Represents a single chunk of the format string, either a literal or one of + the specifiers documented in OpenJDK's javadoc for java.util.Formatter. + This struct is immutable so can be safely shared across threads. */ + private static final class FmtCmpnt { + + private final String _source; + private final byte _conversion; + private final int _argument_index; + private final int _width; + private final int _precision; + private final byte _flags; + + private FmtCmpnt(final String literal) { + this(literal, CONV_LITRL, 0, 0, 0, (byte)0); + } + private FmtCmpnt( + final String src, + final byte conversion, + final int argumentIndex, + final int width, + final int precision, + final byte flags) { + this._source = src; + this._conversion = conversion; + this._argument_index = argumentIndex; + this._width = width; + this._precision = precision; + this._flags = flags; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ ") + .append("source = '").append(_source).append("', ") + .append("conversion = ").append(_conversion).append(", ") + .append("flags = ").append(Byte.toString(_flags, 2)).append(", ") + .append("arg_index = ").append(_argument_index).append(", ") + .append("width = ").append(_width).append(", ") + .append("precision = ").append(_precision).append(", ") + .append("}"); + return sb.toString(); + } + } + + /** + * Helper class for parsing a stream of characters into FmtCmpnt objects. + */ + private static final class FmtSpecBldr { + + private StringBuilder _source; + private byte _conversion; + private int _argument_index; + private int _width; + private int _precision; + private byte _flags; + + private FmtSpecBldr() { + this.init(); + } + + private final void init() { + _argument_index = + _width = + _precision = + _conversion = + _flags = 0; + _source = null; + } + + private final FmtCmpnt build() { + final FmtCmpnt result = new FmtCmpnt( + _source.toString(), + _conversion, + _argument_index, + _width, + _precision, + _flags + ); + init(); + return result; + } + + private final FmtCmpnt append(char c) { + + if (_source == null) { + _source = new StringBuilder(); + } + _source.append(c); + + // FIXME: none of these date formats are implemented, because lazy + // if a datetime is specified, after the conversion character a time + // format specifier is expected. This is the only case where a character + // is allowed after the conversion character. + if (_conversion == CONV_DTIME) switch (c) { + + // Hour of the day for the 24-hour clock, formatted as two digits with a + // leading zero as necessary i.e. 00 - 23. + case 'H': + + // Hour for the 12-hour clock, formatted as two digits with a leading + // zero as necessary, i.e. 01 - 12. + case 'I': + + // Hour of the day for the 24-hour clock, i.e. 0 - 23. + case 'k': + + // Hour for the 12-hour clock, i.e. 1 - 12. + case 'l': + + // Minute within the hour formatted as two digits with a leading zero + // as necessary, i.e. 00 - 59. + case 'M': + + // Seconds within the minute, formatted as two digits with a leading + // zero as necessary, i.e. 00 - 60 ("60" is a special value required to + // support leap seconds). + case 'S': + + // Millisecond within the second formatted as three digits with leading + // zeros as necessary, i.e. 000 - 999. + case 'L': + + // Nanosecond within the second, formatted as nine digits with leading + // zeros as necessary, i.e. 000000000 - 999999999. + case 'N': + + // Locale-specific morning or afternoon marker in lower case, + // e.g."am" or "pm". Use of the conversion prefix 'T' forces this + // output to upper case. + case 'p': + + // RFC 822 style numeric time zone offset from GMT, e.g. -0800. + case 'z': + + // A string representing the abbreviation for the time zone. The + // Formatter's locale will supersede the locale of the argument + // (if any). + case 'Z': + + // Seconds since the beginning of the epoch + // starting at 1 January 1970 00:00:00 UTC, + // i.e. Long.MIN_VALUE/1000 to Long.MAX_VALUE/1000. + case 's': + + // Milliseconds since the beginning of the epoch + // starting at 1 January 1970 00:00:00 UTC, + // i.e. Long.MIN_VALUE to Long.MAX_VALUE. + case 'Q': + + // ------------------------------------------------------------------ + // The following conversion characters are used for formatting dates: + // ------------------------------------------------------------------ + + // Locale-specific full month name, e.g. "January", "February". + case 'B': + + // Locale-specific abbreviated month name, e.g. "Jan", "Feb". + case 'b': + + // Same as 'b'. + case 'h': + + // Locale-specific full name of the day of the week, e.g. "Sunday" + case 'A': + + // Locale-specific short name of the day of the week, e.g. "Sun" + case 'a': + + // Four-digit year divided by 100, formatted as two digits with leading + // zero as necessary, i.e. 00 - 99 + case 'C': + + // Year, formatted as at least four digits with leading zeros + // as necessary, e.g. 0092 equals 92 CE for the Gregorian calendar. + case 'Y': + + // Last two digits of the year, formatted with leading zeros + // as necessary, i.e. 00 - 99. + case 'y': + + // Day of year, formatted as three digits with leading zeros + // as necessary, e.g. 001 - 366 for the Gregorian calendar. + case 'j': + + // Month, formatted as two digits with leading zeros as necessary, + // i.e. 01 - 13. + case 'm': + + // Day of month, formatted as two digits with leading zeros as + // necessary, i.e. 01 - 31 + case 'd': + + // Day of month, formatted as two digits, i.e. 1 - 31. + case 'e': + + // ------------------------------------------------------------------- + // The following conversion characters are used for formatting common + // date/time compositions. + // ------------------------------------------------------------------- + + // Time formatted for the 24-hour clock as "%tH:%tM" + case 'R': + + // Time formatted for the 24-hour clock as "%tH:%tM:%tS". + case 'T': + + // Time formatted for the 12-hour clock as "%tI:%tM:%tS %Tp". The location + // of the morning or afternoon marker ('%Tp') may be locale-dependent. + case 'r': + + // Date formatted as "%tm/%td/%ty". + case 'D': + + // ISO 8601 complete date formatted as "%tY-%tm-%td". + case 'F': + + // Date and time formatted as "%ta %tb %td %tT %tZ %tY", e.g. + // "Sun Jul 20 16:17:00 EDT 1969". + case 'c': + return seal(CONV_DTIME); + + default: + throw new IllegalFormatException("Illegal date/time modifier: " + c); + } + + // -- Flags and Switches ----------------------------------------------- + // these are the possible characters for flags, width, etc. if the input + // is not one of these characters, it needs to be a valid conversion char. + // because the possible flags can vary based on the type of conversion, + // it is easiest to just buffer the flags, argument index, etc. until a + // conversion character has been reached. The seal() method will then + // work out if the specified flags are valid for the given conversion. + switch (c) { + case '$': + case '<': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + case '+': + case '\'': + case ' ': + case '#': + case ',': + case '.': + case '(': + return null; + // -- End of Flags and Switches ---------------------------------------- + + // -- Conversion characters -------------------------------------------- + // If this point is reached, then the current character must be a valid + // conversion character. If it is not, it should fall through the rest + // of the switch statement below and throw an IllegalFormatException + + // string conversion + case 'S': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 's': + return seal(CONV_STRNG); + + // newline conversion + case 'n': + return seal(CONV_NLINE); + + // percent conversion + case '%': + return seal(CONV_PRCNT); + + // decimal numeric conversion + case 'd': + return seal(CONV_DECML); + + // hexidecimal numeric conversion + case 'X': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'x': + return seal(CONV_HXDEC); + + // datetime conversion + case 'T': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 't': + _conversion = CONV_DTIME; + return null; + + // boolean conversion + case 'B': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'b': + return seal(CONV_BOOLN); + + // hashcode conversion + case 'H': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'h': + return seal(CONV_HCODE); + + // character conversion + case 'C': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'c': + return seal(CONV_CHRCT); + + // octal numeric conversion + case 'o': + return seal(CONV_OCTAL); + + // computerized scientific conversion + case 'E': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'e': + return seal(CONV_CPSCI); + + // floating point conversion + case 'f': + return seal(CONV_FLOAT); + + // general scientific floating point conversion + case 'G': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'g': + return seal(CONV_GNSCI); + + // hexidecimal exponential floating point conversion + case 'A': + setFlagTrue(FLAG_FORCE_UPPER_CASE); + case 'a': + return seal(CONV_HXEXP); + // -- End of Conversion characters -------------------------------------- + + default: + throw new IllegalFormatException( + "Invalid character encountered while parsing specifier: " + c); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ ") + .append("source = '").append(_source).append("', ") + .append("conversion = ").append(_conversion).append(", ") + .append("flags = ").append(Byte.toString(_flags, 2)).append(", ") + .append("arg_index = ").append(_argument_index).append(", ") + .append("width = ").append(_width).append(", ") + .append("precision = ").append(_precision).append(", ") + .append("}"); + return sb.toString(); + } + + private final FmtCmpnt seal(final byte conversion) { + + // throwing IllegalStateException instead of IllegalFormatException + // because this should only occur if there is a bug in the append() + // method. In that case I'd prefer to fail fast even if the user is + // explicitly trying to catch IllegalFormatException. + if (conversion < 0 || conversion > 0xE) { + throw new IllegalArgumentException(); + } + + this._conversion = conversion; + + // if the length is less than 2, it must mean that only the conversion + // character was specified. Since a conversion character by itself is a + // valid pattern, just build and return + if (_source.length() < 2) { + return build(); + } + + // --------------------------------------------------------------------- + // spec format: [argument_index$][flags][width][.precision] + // --------------------------------------------------------------------- + // the last character of the spec is the conversion character which has + // already been translated the appropriate byte by the append() method, + // so the last character gets chopped off before processing + final String spec = _source.substring(0, _source.length() - 1); + + // if argument index is supported, it should be followed by a '$' and be + // comprised only of digit characters, or it should be a single '<' char + final int dollarIndex = spec.indexOf('$'); + if (dollarIndex > -1) { + if (acceptsArgument(conversion)) { + if (spec.charAt(dollarIndex - 1) == '<') { + _argument_index = AIDX_PREV; + } else { + _argument_index = Integer.valueOf(spec.substring(0, dollarIndex)); + } + } else { + throw new IllegalFormatException( + "Formats that do not accept arguments cannot specify an index." + ); + } + } + if (dollarIndex == (spec.length() - 1)) { + return build(); + } + + // if precision is supported, look for the first period and assume that + // everything before is the width and everything after is the precision + final int dotIndex = spec.indexOf('.'); + if (dotIndex > -1) { + if (precisionSupported(conversion)) { + _precision = Integer.valueOf(spec.substring(dotIndex + 1)); + } else { + throw new IllegalFormatException( + "Precision is not supported for " + conversion + ); + } + } + + // Now loop over the remaining characters to get the width as well as any + // applicable flags. Note: 0 is a valid flag so must be handled carefully + final String remaining = spec.substring( + Math.max(dollarIndex, 0), dotIndex > -1 ? dotIndex : spec.length() + ); + int flagsEnd = -1; + for (int i = 0, n = remaining.length(); i < n && (flagsEnd == -1); i++) { + final char c = remaining.charAt(i); + switch (c) { + case '-': + ensureLeftJustifySupported(); + setFlagTrue(FLAG_LEFT_JUSTIFIED); + break; + case '#': + ensureNumeric(c); + setFlagTrue(FLAG_ALTERNATE_FORM); + break; + case '+': + ensureNumeric(c); + setFlagTrue(FLAG_ALWAYS_INCLUDES_SIGN); + break; + case ' ': + ensureNumeric(c); + setFlagTrue(FLAG_LEADING_SPACE_PADDED); + break; + case ',': + ensureNumeric(c); + setFlagTrue(FLAG_GROUPING_SEPARATORS); + break; + case '(': + ensureNumeric(c); + setFlagTrue(FLAG_NEGATIVES_IN_PARENS); + break; + case '0': + ensureNumeric(c); + setFlagTrue(FLAG_LEADING_ZERO_PADDED); + break; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + flagsEnd = i; + _width = Integer.valueOf(remaining.substring(flagsEnd)); + return build(); + } + } + throw new IllegalStateException(); + } + private final void ensureLeftJustifySupported() { + if (!widthSupported(_conversion)) { + throw new IllegalFormatException( + "Conversion must support width if specifying left justified." + ); + } + } + private final void ensureNumeric(final char c) { + if (!isNumeric(_conversion)) { + throw new IllegalFormatException( + "flag " + c + " only supported on numeric specifiers." + ); + } + } + private final void setFlagTrue(final byte flag) { + _flags |= flag; + }/* + final void setFlagFalse(final byte flag) { + _flags &= ~flag; + }*/ + } +} diff --git a/classpath/java/io/PrintStream.java b/classpath/java/io/PrintStream.java index 7e66c177b3..1a976e16f7 100644 --- a/classpath/java/io/PrintStream.java +++ b/classpath/java/io/PrintStream.java @@ -78,6 +78,25 @@ public class PrintStream extends OutputStream { print(String.valueOf(s)); } + public synchronized void printf(java.util.Locale locale, String format, Object... args) { + // should this be cached in an instance variable?? + final java.util.Formatter formatter = new java.util.Formatter(this); + formatter.format(locale, format, args); + } + + public synchronized void printf(String format, Object... args) { + final java.util.Formatter formatter = new java.util.Formatter(this); + formatter.format(format, args); + } + + public void format(String format, Object... args) { + printf(format, args); + } + + public void format(java.util.Locale locale, String format, Object... args) { + printf(locale, format, args); + } + public synchronized void println(String s) { try { out.write(s.getBytes()); diff --git a/classpath/java/io/Writer.java b/classpath/java/io/Writer.java index 1726192e7a..60f0ccdeaf 100644 --- a/classpath/java/io/Writer.java +++ b/classpath/java/io/Writer.java @@ -10,7 +10,7 @@ package java.io; -public abstract class Writer implements Closeable, Flushable { +public abstract class Writer implements Closeable, Flushable, Appendable { public void write(int c) throws IOException { char[] buffer = new char[] { (char) c }; write(buffer); @@ -33,6 +33,30 @@ public abstract class Writer implements Closeable, Flushable { public abstract void write(char[] buffer, int offset, int length) throws IOException; + public Appendable append(final char c) throws IOException { + write((int)c); + return this; + } + + public Appendable append(final CharSequence sequence) throws IOException { + return append(sequence, 0, sequence.length()); + } + + public Appendable append(CharSequence sequence, int start, int end) + throws IOException { + final int length = end - start; + if (sequence instanceof String) { + write((String)sequence, start, length); + } else { + final char[] charArray = new char[length]; + for (int i = start; i < end; i++) { + charArray[i] = sequence.charAt(i); + } + write(charArray, 0, length); + } + return this; + } + public abstract void flush() throws IOException; public abstract void close() throws IOException; diff --git a/classpath/java/lang/String.java b/classpath/java/lang/String.java index 7bfc4dda44..be45b3c464 100644 --- a/classpath/java/lang/String.java +++ b/classpath/java/lang/String.java @@ -13,6 +13,7 @@ package java.lang; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.Comparator; +import java.util.Formatter; import java.util.Locale; import java.util.regex.Pattern; @@ -574,6 +575,20 @@ public final class String public native String intern(); + public static String format(String fmt, Object... args) { + final Formatter formatter = new Formatter(); + final String result = formatter.format(fmt, args).toString(); + formatter.close(); + return result; + } + + public static String format(Locale l, String fmt, Object... args) { + final Formatter formatter = new Formatter(); + final String result = formatter.format(l, fmt, args).toString(); + formatter.close(); + return result; + } + public static String valueOf(Object s) { return s == null ? "null" : s.toString(); } diff --git a/classpath/java/util/Formatter.java b/classpath/java/util/Formatter.java new file mode 100644 index 0000000000..12d5919e82 --- /dev/null +++ b/classpath/java/util/Formatter.java @@ -0,0 +1,158 @@ +/* Copyright (c) 2008-2015, Avian Contributors + + Permission to use, copy, modify, and/or distribute this software + for any purpose with or without fee is hereby granted, provided + that the above copyright notice and this permission notice appear + in all copies. + + There is NO WARRANTY for this software. See license.txt for + details. */ + +package java.util; + +import java.io.Closeable; +import java.io.File; +import java.io.FileWriter; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.Writer; + +import avian.FormatString; + +public class Formatter implements Closeable, Flushable, AutoCloseable { + + private final Appendable _out; + + private final Locale _locale; + + private IOException lastException; + + private boolean _closed; + + private void ensureNotClosed() { + if (this._closed) { + throw new IllegalStateException(); + } + } + + public Formatter() { + this((Appendable) null, (Locale) null); + } + + public Formatter(final Appendable a) { + this(a, null); + } + + public Formatter(final Appendable a, final Locale l) { + if (a == null) { + this._out = new StringBuilder(); + } else { + this._out = a; + } + if (l == null) { + this._locale = Locale.getDefault(); + } else { + this._locale = l; + } + } + + public Formatter(File file) throws IOException { + this(file, null); + } + + public Formatter(File file, Locale l) throws IOException { + this(new FileWriter(file), l); + } + + public Formatter(Locale l) { + this((Appendable) null, l); + } + + public Formatter(OutputStream os) { + this(os, null); + } + + public Formatter(OutputStream os, Locale l) { + this(new OutputStreamWriter(os), l); + } + + public Formatter(PrintStream ps) { + this(new OutputStreamWriter(ps)); + } + + public Formatter(String fileName) throws IOException { + this(fileName, (Locale) null); + } + + public Formatter(String fileName, Locale l) throws IOException { + this(new File(fileName), l); + } + + public void close() { + if (this._closed) { + return; + } + final Appendable out = out(); + if (out instanceof Closeable) { + final Closeable closeable = (Closeable) out; + try { + closeable.close(); + } catch (IOException e) { + throw new RuntimeException("An error occurred while closing.", e); + } + this._closed = true; + } + } + + public void flush() { + ensureNotClosed(); + final Appendable out = out(); + if (out instanceof Flushable) { + final Flushable flushable = (Flushable) out; + try { + flushable.flush(); + } catch (IOException e) { + throw new RuntimeException("An error occurred while flushing.", e); + } + } + } + + public Formatter format(Locale l, final String format, final Object...args) { + ensureNotClosed(); + try { + final FormatString formatString = FormatString.compile(format); + this._out.append(formatString.format(args)); + } catch (IOException e) { + this.lastException = e; + } + return this; + } + + public Formatter format(String format, Object... args) { + ensureNotClosed(); + return this.format(null, format, args); + } + + public IOException ioException() { + return lastException; + } + + public Locale locale() { + ensureNotClosed(); + return this._locale; + } + + public Appendable out() { + ensureNotClosed(); + return this._out; + } + + public String toString() { + ensureNotClosed(); + return this._out.toString(); + } + +} diff --git a/classpath/java/util/IllegalFormatException.java b/classpath/java/util/IllegalFormatException.java new file mode 100644 index 0000000000..d9f24f4564 --- /dev/null +++ b/classpath/java/util/IllegalFormatException.java @@ -0,0 +1,31 @@ +/* Copyright (c) 2008-2015, Avian Contributors + + Permission to use, copy, modify, and/or distribute this software + for any purpose with or without fee is hereby granted, provided + that the above copyright notice and this permission notice appear + in all copies. + + There is NO WARRANTY for this software. See license.txt for + details. */ + +package java.util; + +public class IllegalFormatException extends IllegalArgumentException { + + public IllegalFormatException() { + super(); + } + + public IllegalFormatException(String message, Throwable cause) { + super(message, cause); + } + + public IllegalFormatException(String message) { + super(message); + } + + public IllegalFormatException(Throwable cause) { + super(cause); + } + +} diff --git a/test/FormatStrings.java b/test/FormatStrings.java new file mode 100644 index 0000000000..799840e277 --- /dev/null +++ b/test/FormatStrings.java @@ -0,0 +1,304 @@ +/* Copyright (c) 2008-2015, Avian Contributors + + Permission to use, copy, modify, and/or distribute this software + for any purpose with or without fee is hereby granted, provided + that the above copyright notice and this permission notice appear + in all copies. + + There is NO WARRANTY for this software. See license.txt for + details. */ + +import avian.FormatString; +import java.net.URL; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/* + * @author bcg + */ +public class FormatStrings { + + public static void main(String... args) throws Exception { + FormatStrings test = new FormatStrings(); + test.testLiteral(); + test.testString(); + test.testNewline(); + test.testPercent(); + test.testBoolean(); + test.testCharacter(); + test.testHashCode(); + test.testIntegers(); + test.testWidths(); + test.testPrecisions(); + } + + private void _testFormat(String expected, String format, Object... args) { + String actual = FormatString.compile(format).format(args); + ensureEquals(expected, actual); + System.err.println("Expected: " + expected + ", Actual: " + actual); + } + + private static void ensureEquals(String expected, String actual) { + if (expected != actual) { + if ((expected == null || actual == null) || !(expected.equals(actual))) { + throw new IllegalArgumentException( + "Expected `" + expected + "` but was actually `" + actual + "`."); + } + } + } + + public void testLiteral() { + _testFormat("test Literal 1", "test Literal 1"); + _testFormat("test Literal 2", "test Literal 2", (Object[]) null); + _testFormat("test Literal 3", "test Literal 3", new Object[0]); + } + + public void testString() { + _testFormat("test String 1", "test %s", "String 1"); + _testFormat("test String null", "test String %s", new Object[]{null} ); + _testFormat("test String 2", "test %2$s", "String 1", "String 2"); + _testFormat("String `string`", "String `%s`", new String("string")); + _testFormat("String `STRING`", "String `%S`", new String("string")); + _testFormat("String `another string`", "String `%s`", new String("another string")); + _testFormat("String `ANOTHER STRING`", "String `%S`", new String("another string")); + _testFormat("String `null`", "String `%s`", (String)null); + _testFormat("String `NULL`", "String `%S`", (String)null); + _testFormat("String `true`", "String `%s`", new Boolean("true")); + _testFormat("String `TRUE`", "String `%S`", new Boolean("true")); + _testFormat("String `false`", "String `%s`", new Boolean("false")); + _testFormat("String `FALSE`", "String `%S`", new Boolean("false")); + } + + public void testNewline() { + final String newline = System.getProperty("line.separator"); + _testFormat( + "<<<" + newline + " test newlines" + newline + ">>>", + "<<<%n test newlines%n>>>" + ); + } + + public void testBoolean() { + _testFormat("test boolean true", "test boolean %b", true); + _testFormat("test Boolean true", "test Boolean %b", Boolean.TRUE); + _testFormat("test boolean false", "test boolean %b", false); + _testFormat("test Boolean false", "test Boolean %b", Boolean.FALSE); + _testFormat("test null Boolean (false)", "test null Boolean (%b)", new Object[]{(Boolean)null}); + _testFormat("test non-null Boolean (true)", "test non-null Boolean (%b)", new Object()); + _testFormat("test boolean string (true)", "test boolean string (%b)", "false"); + _testFormat("Boolean `true`", "Boolean `%b`", new Boolean("true")); + _testFormat("Boolean `TRUE`", "Boolean `%B`", new Boolean("true")); + _testFormat("Boolean `false`", "Boolean `%b`", new Boolean("false")); + _testFormat("Boolean `FALSE`", "Boolean `%B`", new Boolean("false")); + _testFormat("Boolean `false`", "Boolean `%b`", (String)null); + _testFormat("Boolean `FALSE`", "Boolean `%B`", (String)null); + _testFormat("Boolean `true`", "Boolean `%b`", new String("")); + _testFormat("Boolean `TRUE`", "Boolean `%B`", new String("")); + _testFormat("Boolean `true`", "Boolean `%b`", new String("true")); + _testFormat("Boolean `TRUE`", "Boolean `%B`", new String("true")); + _testFormat("Boolean `true`", "Boolean `%b`", new String("false")); + _testFormat("Boolean `TRUE`", "Boolean `%B`", new String("false")); + } + + public void testPercent() { + _testFormat("Percents work 100%", "Percents work 100%%"); + } + + public void testCharacter() { + _testFormat("test character such as a", "test character such as %c", 'a'); + _testFormat("test character such as b", "test character such as %c", (int) 98); + _testFormat("test character such as c", "test character such as %c", (byte) 99); + _testFormat("test character such as d", "test character such as %c", (short) 100); + } + + public void testHashCode() { + final Object obj1 = new Object(); + final Object obj2 = new Object(); + final String hc1 = Integer.toHexString(obj1.hashCode()); + final String hc2 = Integer.toHexString(obj2.hashCode()); + _testFormat("test hashcode 1 (" + hc1 + ")" , "test hashcode 1 (%h)", obj1, obj2); + _testFormat("test hashcode 2 (" + hc2 + ")" , "test hashcode 2 (%2$h)", obj1, obj2); + _testFormat("test hashcode null", "test hashcode %h", (String) null); + } + + public void testIntegers() { + + _testFormat("Long 1", "Long %d", new Long(1)); + _testFormat("Long 2", "Long %2$d", new Long(1), new Long(2)); + _testFormat("Integer 1", "Integer %d", new Integer(1)); + _testFormat("Integer 2", "Integer %2$d", new Integer(1), new Integer(2)); + _testFormat("Short 1", "Short %d", new Short((short)1)); + _testFormat("Short 2", "Short %2$d", new Short((short)1), new Short((short)2)); + _testFormat("Byte 1", "Byte %d", new Byte((byte)1)); + _testFormat("Byte 2", "Byte %2$d", new Byte((byte)1), new Byte((byte)2)); + + _testFormat("Long 144", "Long %o", new Long(100)); + _testFormat("Long 310", "Long %2$o", new Long(100), new Long(200)); + _testFormat("Integer 144", "Integer %o", new Integer(100)); + _testFormat("Integer 310", "Integer %2$o", new Integer(100), new Integer(200)); + _testFormat("Short 144", "Short %o", new Short((short)100)); + _testFormat("Short 310", "Short %2$o", new Short((short)100), new Short((short)200)); + _testFormat("Byte 144", "Byte %o", new Byte((byte)100)); + _testFormat("Byte 310", "Byte %2$o", new Byte((byte)100), new Byte((byte)200)); + + _testFormat("Long 64", "Long %x", new Long(100)); + _testFormat("Long c8", "Long %2$x", new Long(100), new Long(200)); + _testFormat("Long C8", "Long %2$X", new Long(100), new Long(200)); + _testFormat("Integer 64", "Integer %x", new Integer(100)); + _testFormat("Integer c8", "Integer %2$x", new Integer(100), new Integer(200)); + _testFormat("Short 64", "Short %x", new Short((short)100)); + _testFormat("Short C8", "Short %2$X", new Short((short)100), new Short((short)200)); + _testFormat("Byte 64", "Byte %x", new Byte((byte)100)); + _testFormat("Byte c8", "Byte %2$x", new Byte((byte)100), new Byte((byte)200)); + + _testFormat("Decimal `1`", "Decimal `%d`", new Integer((int)1)); + _testFormat("Decimal `0`", "Decimal `%d`", new Integer((int)0)); + _testFormat("Decimal `100`", "Decimal `%d`", new Integer((int)100)); + _testFormat("Decimal `100000`", "Decimal `%d`", new Integer((int)100000)); + _testFormat("Decimal `63`", "Decimal `%d`", new Integer((int)63)); + _testFormat("Decimal `64`", "Decimal `%d`", new Integer((int)64)); + _testFormat("Decimal `-1`", "Decimal `%d`", new Integer((int)-1)); + _testFormat("Decimal `-100`", "Decimal `%d`", new Integer((int)-100)); + _testFormat("Decimal `-100000`", "Decimal `%d`", new Integer((int)-100000)); + _testFormat("Decimal `1`", "Decimal `%d`", new Byte((byte)1)); + _testFormat("Decimal `0`", "Decimal `%d`", new Byte((byte)0)); + _testFormat("Decimal `100`", "Decimal `%d`", new Byte((byte)100)); + _testFormat("Decimal `63`", "Decimal `%d`", new Byte((byte)63)); + _testFormat("Decimal `64`", "Decimal `%d`", new Byte((byte)64)); + _testFormat("Decimal `-1`", "Decimal `%d`", new Byte((byte)-1)); + _testFormat("Decimal `-100`", "Decimal `%d`", new Byte((byte)-100)); + _testFormat("Decimal `1`", "Decimal `%d`", new Long((long)1)); + _testFormat("Decimal `0`", "Decimal `%d`", new Long((long)0)); + _testFormat("Decimal `100`", "Decimal `%d`", new Long((long)100)); + _testFormat("Decimal `100000`", "Decimal `%d`", new Long((long)100000)); + _testFormat("Decimal `63`", "Decimal `%d`", new Long((long)63)); + _testFormat("Decimal `64`", "Decimal `%d`", new Long((long)64)); + _testFormat("Decimal `-1`", "Decimal `%d`", new Long((long)-1)); + _testFormat("Decimal `-100`", "Decimal `%d`", new Long((long)-100)); + _testFormat("Decimal `-100000`", "Decimal `%d`", new Long((long)-100000)); + _testFormat("Decimal `1`", "Decimal `%d`", new Short((short)1)); + _testFormat("Decimal `0`", "Decimal `%d`", new Short((short)0)); + _testFormat("Decimal `100`", "Decimal `%d`", new Short((short)100)); + _testFormat("Decimal `63`", "Decimal `%d`", new Short((short)63)); + _testFormat("Decimal `64`", "Decimal `%d`", new Short((short)64)); + _testFormat("Decimal `-1`", "Decimal `%d`", new Short((short)-1)); + _testFormat("Decimal `-100`", "Decimal `%d`", new Short((short)-100)); + + _testFormat("Octal `1`", "Octal `%o`", new Integer((int)1)); + _testFormat("Octal `0`", "Octal `%o`", new Integer((int)0)); + _testFormat("Octal `144`", "Octal `%o`", new Integer((int)100)); + _testFormat("Octal `303240`", "Octal `%o`", new Integer((int)100000)); + _testFormat("Octal `77`", "Octal `%o`", new Integer((int)63)); + _testFormat("Octal `100`", "Octal `%o`", new Integer((int)64)); + _testFormat("Octal `37777777777`", "Octal `%o`", new Integer((int)-1)); + _testFormat("Octal `37777777634`", "Octal `%o`", new Integer((int)-100)); + _testFormat("Octal `37777474540`", "Octal `%o`", new Integer((int)-100000)); + _testFormat("Octal `1`", "Octal `%o`", new Byte((byte)1)); + _testFormat("Octal `0`", "Octal `%o`", new Byte((byte)0)); + _testFormat("Octal `144`", "Octal `%o`", new Byte((byte)100)); + _testFormat("Octal `77`", "Octal `%o`", new Byte((byte)63)); + _testFormat("Octal `100`", "Octal `%o`", new Byte((byte)64)); + _testFormat("Octal `377`", "Octal `%o`", new Byte((byte)-1)); + _testFormat("Octal `234`", "Octal `%o`", new Byte((byte)-100)); + _testFormat("Octal `1`", "Octal `%o`", new Long((long)1)); + _testFormat("Octal `0`", "Octal `%o`", new Long((long)0)); + _testFormat("Octal `144`", "Octal `%o`", new Long((long)100)); + _testFormat("Octal `303240`", "Octal `%o`", new Long((long)100000)); + _testFormat("Octal `77`", "Octal `%o`", new Long((long)63)); + _testFormat("Octal `100`", "Octal `%o`", new Long((long)64)); + _testFormat("Octal `1`", "Octal `%o`", new Short((short)1)); + _testFormat("Octal `0`", "Octal `%o`", new Short((short)0)); + _testFormat("Octal `144`", "Octal `%o`", new Short((short)100)); + _testFormat("Octal `77`", "Octal `%o`", new Short((short)63)); + _testFormat("Octal `100`", "Octal `%o`", new Short((short)64)); + _testFormat("Octal `177777`", "Octal `%o`", new Short((short)-1)); + _testFormat("Octal `177634`", "Octal `%o`", new Short((short)-100)); + + _testFormat("HexDec `1`", "HexDec `%x`", new Integer((int)1)); + _testFormat("HexDec `1`", "HexDec `%X`", new Integer((int)1)); + _testFormat("HexDec `0`", "HexDec `%x`", new Integer((int)0)); + _testFormat("HexDec `0`", "HexDec `%X`", new Integer((int)0)); + _testFormat("HexDec `64`", "HexDec `%x`", new Integer((int)100)); + _testFormat("HexDec `64`", "HexDec `%X`", new Integer((int)100)); + _testFormat("HexDec `186a0`", "HexDec `%x`", new Integer((int)100000)); + _testFormat("HexDec `186A0`", "HexDec `%X`", new Integer((int)100000)); + _testFormat("HexDec `3f`", "HexDec `%x`", new Integer((int)63)); + _testFormat("HexDec `3F`", "HexDec `%X`", new Integer((int)63)); + _testFormat("HexDec `40`", "HexDec `%x`", new Integer((int)64)); + _testFormat("HexDec `40`", "HexDec `%X`", new Integer((int)64)); + _testFormat("HexDec `ffffffff`", "HexDec `%x`", new Integer((int)-1)); + _testFormat("HexDec `FFFFFFFF`", "HexDec `%X`", new Integer((int)-1)); + _testFormat("HexDec `ffffff9c`", "HexDec `%x`", new Integer((int)-100)); + _testFormat("HexDec `FFFFFF9C`", "HexDec `%X`", new Integer((int)-100)); + _testFormat("HexDec `fffe7960`", "HexDec `%x`", new Integer((int)-100000)); + _testFormat("HexDec `FFFE7960`", "HexDec `%X`", new Integer((int)-100000)); + _testFormat("HexDec `1`", "HexDec `%x`", new Byte((byte)1)); + _testFormat("HexDec `1`", "HexDec `%X`", new Byte((byte)1)); + _testFormat("HexDec `0`", "HexDec `%x`", new Byte((byte)0)); + _testFormat("HexDec `0`", "HexDec `%X`", new Byte((byte)0)); + _testFormat("HexDec `64`", "HexDec `%x`", new Byte((byte)100)); + _testFormat("HexDec `64`", "HexDec `%X`", new Byte((byte)100)); + _testFormat("HexDec `3f`", "HexDec `%x`", new Byte((byte)63)); + _testFormat("HexDec `3F`", "HexDec `%X`", new Byte((byte)63)); + _testFormat("HexDec `40`", "HexDec `%x`", new Byte((byte)64)); + _testFormat("HexDec `40`", "HexDec `%X`", new Byte((byte)64)); + _testFormat("HexDec `ff`", "HexDec `%x`", new Byte((byte)-1)); + _testFormat("HexDec `FF`", "HexDec `%X`", new Byte((byte)-1)); + _testFormat("HexDec `9c`", "HexDec `%x`", new Byte((byte)-100)); + _testFormat("HexDec `9C`", "HexDec `%X`", new Byte((byte)-100)); + _testFormat("HexDec `1`", "HexDec `%x`", new Long((long)1)); + _testFormat("HexDec `1`", "HexDec `%X`", new Long((long)1)); + _testFormat("HexDec `0`", "HexDec `%x`", new Long((long)0)); + _testFormat("HexDec `0`", "HexDec `%X`", new Long((long)0)); + _testFormat("HexDec `64`", "HexDec `%x`", new Long((long)100)); + _testFormat("HexDec `64`", "HexDec `%X`", new Long((long)100)); + _testFormat("HexDec `186a0`", "HexDec `%x`", new Long((long)100000)); + _testFormat("HexDec `186A0`", "HexDec `%X`", new Long((long)100000)); + _testFormat("HexDec `3f`", "HexDec `%x`", new Long((long)63)); + _testFormat("HexDec `3F`", "HexDec `%X`", new Long((long)63)); + _testFormat("HexDec `40`", "HexDec `%x`", new Long((long)64)); + _testFormat("HexDec `40`", "HexDec `%X`", new Long((long)64)); + _testFormat("HexDec `ffffffffffffffff`", "HexDec `%x`", new Long((long)-1)); + _testFormat("HexDec `FFFFFFFFFFFFFFFF`", "HexDec `%X`", new Long((long)-1)); + _testFormat("HexDec `ffffffffffffff9c`", "HexDec `%x`", new Long((long)-100)); + _testFormat("HexDec `FFFFFFFFFFFFFF9C`", "HexDec `%X`", new Long((long)-100)); + _testFormat("HexDec `fffffffffffe7960`", "HexDec `%x`", new Long((long)-100000)); + _testFormat("HexDec `FFFFFFFFFFFE7960`", "HexDec `%X`", new Long((long)-100000)); + _testFormat("HexDec `1`", "HexDec `%x`", new Short((short)1)); + _testFormat("HexDec `1`", "HexDec `%X`", new Short((short)1)); + _testFormat("HexDec `0`", "HexDec `%x`", new Short((short)0)); + _testFormat("HexDec `0`", "HexDec `%X`", new Short((short)0)); + _testFormat("HexDec `64`", "HexDec `%x`", new Short((short)100)); + _testFormat("HexDec `64`", "HexDec `%X`", new Short((short)100)); + _testFormat("HexDec `3f`", "HexDec `%x`", new Short((short)63)); + _testFormat("HexDec `3F`", "HexDec `%X`", new Short((short)63)); + _testFormat("HexDec `40`", "HexDec `%x`", new Short((short)64)); + _testFormat("HexDec `40`", "HexDec `%X`", new Short((short)64)); + _testFormat("HexDec `ffff`", "HexDec `%x`", new Short((short)-1)); + _testFormat("HexDec `FFFF`", "HexDec `%X`", new Short((short)-1)); + _testFormat("HexDec `ff9c`", "HexDec `%x`", new Short((short)-100)); + _testFormat("HexDec `FF9C`", "HexDec `%X`", new Short((short)-100)); + } + + public void testWidths() { + _testFormat("0001", "%04d", 1); + _testFormat(" 1", "%4d", 1); + _testFormat(" 11", "%4x", 17); + _testFormat("0011", "%04x", 17); + _testFormat(" a", "%2x", 10); + _testFormat(" A", "%2X", 10); + _testFormat("a ", "%-2x", 10); + _testFormat("A ", "%-2X", 10); + _testFormat("10000", "%4d", 10000); + _testFormat("Hello World ", "%-15s", "Hello World"); + _testFormat(" Hello World", "%15s", "Hello World"); + } + + public void testPrecisions() { + _testFormat("Hello", "%-1.5s", "Hello World"); + _testFormat("Hello", "%1.5s", "Hello World"); + } + +}