All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.divxdede.text.formatter.PatternFormatter Maven / Gradle / Ivy

/*
 * Copyright (c) 2010 INFASS Syst?mes (http://www.infass.com) All rights reserved.
 * PatternFormatter.java is a part of this Commons library
 * ====================================================================
 *
 * Commons library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or any later version.
 *
 * This is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see .
 */
package org.divxdede.text.formatter;

import java.text.DateFormatSymbols;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Formattable;
import java.util.Formatter;
import java.util.HashMap;
import java.util.IllegalFormatCodePointException;
import java.util.IllegalFormatConversionException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingFormatArgumentException;
import java.util.TimeZone;
import sun.misc.FormattedFloatingDecimal; // too complex to re-implement it...

/**
 * PatternFormatter is a printf-style string formatting implementation like {@link java.util.Formatter Formatter}.
 * 

Objectives

* If this class has closely the same expressivity than the sun's implementation, PatternFormatter is expected to be more flexible and efficient: *
    *
  • Pattern can be compiled for multiple reuse. Compile feature avoid pattern parsing each formatting requests. * This design is intended to increase performance when formatting always the same pattern with differents arguments.
  • * *
  • ArgumentNameSpace provide a way to custom argument's identifier for create more specific pattern's formatter. *
*

Differences with {@link java.util.Formatter Formatter}

* This class behaviour may have slightly differences with the {@link java.util.Formatter} implementation. * The most of all is that all inapplicables flags are silently ignored instead of throwing an exception. *

* Numerics conversions accept all number's type for any conversion's modifier. *

    *
  • %d (and similare) accept any floating numbers like float,Float,double,Double,BigDecimal but theses numbers will be converted to a long value
  • *
  • %f (and similare) accept any non-floating numbers like byte,Byte,short,Short,int,Integer,long,Long,BigInteger but theses numbers will be converted to a double value
  • *
  • BigInteger and BigDecimal are not handled for print theses extra-capacity like {@link java.util.Formatter} does
  • *
*

*

Argument's specification

* An argument specification has an expression form like it: *
 *   %[argument_index$][flags][width][.precision]conversion[datetime-conversion]
 * 
*
    *
  • argument_index can be an explicit 1-based index or '<' for reuse the precedent argument formatted.
  • *
  • flags can be - # + 0 , ( and space ' '
  • *
  • width must be an integer
  • *
  • precision must be an integer
  • *
  • conversion can be a conversion modifier d,o,x,X,e,E,g,G,f,a,A,c,C,t,T,b,B,s,S,h,H
  • *
  • datetime-conversion can be a conversion modifier H,I,k,l,M,N,L,Q,p,s,S,T,z,Z,a,A,b,B,C,d,e,h,j,m,y,Y,r,R,c,D,F
  • *
*

* Formattable interface is respected with %s or %S conversions. But this interface requires a Formatter * then we create it when it's needed. The formatter is 'empty' and it just serve to get the result of the #formatTo method. *

* For more documentations of applicable patterns, flags, width, precision and conversion's modifiers details, * please refer to the {@link java.util.Formatter} *

Simple examples

* Example without use of compile feature: *
 *      String stringA = PatternFormatter.format("Your name is %s and you are %d year old" , "John doe" , 25 );
 *      String stringB = PatternFormatter.format("Your name is %s and you are %d year old" , "Dupont" , 39 );   // the pattern is parsed a new time...
 * 
*

* Example with use of compile feature: *

 *      Pattern pattern = PatternFormatter.compile("Your name is %s and you are %d year old");
 *
 *      String stringA = PatternFormatter.format(pattern , "John doe" , 25 );
 *      String stringB = PatternFormatter.format(pattern , "Dupont" , 39 );   // the compiled-pattern is reused
 * 
*

NameSpaces and customization

* An argument namespace is a way to provide custom identifiers instead of standards one. Theses identifiers can have a length greater than one character. * The namespace must define which standard conversion's type theses custom identifiers it refer. * And as a second goal, a namespace can control how to bind an identifier and it's argument. For achieve this binding, some interfaces can be used: *
    *
  • IndexableArgumentNameSpace: this namespace bind an identifier to it's argument by providing the 0-index position of the requested argument
  • *
  • AttributeArgumentNameSpace: this namespace bind an identifier to a particular property of the argument choice with standard pattern's rules
  • *
  • DetailedArgumentNameSpace: this namespace can control more informations from arguments like flags,width and precision
  • *
* Two implementations of theses interfaces are provided for an easy-use of theses: *
    *
  • DefaultArgumentNameSpace providing the standard namespace support
  • *
  • CompositeArgumentNameSpace providing a way to compose a namespace from differents ones (like DefaultArgumentNameSpace)
  • *
*

* A pattern using an IndexableArgumentNameSpace can't use anymore [argument_index$] specification like "%2$d" and will throw an exception.
* A pattern using an AttributeArgumentNameSpace can use [argument_index$] for define which argument must be used for getting the underlying attribute.
* A pattern using an DetailedArgumentNameSpace can use [argument_index$] for define which argument must be used too.
*

* Example of a color namespace: *

 *   public class ColorArgumentNameSpace implements AttributeArgumentNameSpace , DetailedArgumentNameSpace {
 *
 *          // sub formatter required for print %rgb and %RGB
 *          private Pattern          rgbPattern    = PatternFormatter.compile("#%red%1$green%1$blue",this);
 *          private Pattern          rgbPatternHex = PatternFormatter.compile("#%RED%1$GREEN%1$BLUE",this);
 *
 *          public String[] symbols() {
 *              return new String[]{ "red" , "green" , "blue" , "rgb" , "alpha" , "RED" , "GREEN" , "BLUE" , "RGB" , "ALPHA" };
 *          }
 *
 *          // return requested color's attribute
 *          public Object getAtttribute(Object value, String symbol , Locale locale ) {
 *              if( value instanceof Color ) {
 *                  Color c = (Color)value;
 *                  if( symbol.equalsIgnoreCase("red") )   return c.getRed();
 *                  if( symbol.equalsIgnoreCase("green") ) return c.getGreen();
 *                  if( symbol.equalsIgnoreCase("blue") )  return c.getBlue();
 *                  if( symbol.equalsIgnoreCase("alpha") ) return c.getAlpha();
 *                  if( symbol.equals("rgb") )             return PatternFormatter.format( rgbPattern , locale, value );
 *                  if( symbol.equals("RGB") )             return PatternFormatter.format( rgbPatternHex , locale, value );
 *              }
 *
 *              return value;
 *          }
 *
 *          // set default width and zero padding
 *          public ArgumentDetails getDetails(ArgumentDetails details, String symbol) {
 *              if( getConversion(symbol) == Conversion.DECIMAL_INTEGER ) {
 *                  if( details.getWidth() < 0 ) {
 *                      details.setWidth(3);                                            // minimum 3 digits if not specified
 *                      details.setFlags( details.getFlags().and( Flags.ZERO_PAD ) );   // digits completed with '0'
 *                  }
 *              }
 *              if( getConversion(symbol) == Conversion.HEXADECIMAL_INTEGER ) {
 *                  if( details.getWidth() < 0 ) {
 *                      details.setWidth(2);                                            // minimum 2 digits if not specified
 *                      details.setFlags( details.getFlags().and( Flags.ZERO_PAD ) );   // digits completed with '0'
 *                  }
 *              }
 *              return details;
 *          }
 *
 *          // decimal/hexadecimal/string
 *          public Conversion getConversion(String symbol) {
 *              if( symbol.equalsIgnoreCase("rgb") ) {
 *                  return Conversion.STRING;
 *              }
 *              if( Character.isUpperCase( symbol.charAt(0) ) ) return Conversion.HEXADECIMAL_INTEGER;
 *              return Conversion.DECIMAL_INTEGER;
 *         }
 *
 *          public void use(String symbol) {}
 *          public String getDateSeparator(String symbol) { return null; }
 *      }
 * 
* And can be used like it: *
 *      // create an extended namespace that can use standard argument and new identifiers for color objects
 *      ArgumentNameSpace extendedNameSpace = new CompositeArgumentNameSpace( new ColorArgumentNameSpace() , new DefaultArgumentNameSpace() );
 *
 *      // compile our pattern with our custom namespace
 *      Pattern pattern = PatternFormatter.compile("I got color %RGB the %tc" , extendedNameSpace );
 *
 *      // format one
 *      System.out.println( PatternFormatter.format( pattern , Color.RED , System.currentTimeMillis() ) );
 * 
 * 
* That give an output like this sample: *
 *      I got color #ff0000 the lun. ao?t 31 21:33:36 CEST 2009
 * 
* * @author Andr? S?bastien (divxdede) * @see java.util.Formatter * @since 0.2 */ public class PatternFormatter { /** An arguments namespace allow to define custom conversion's identifiers instead of using defaults.
* With a namespace, conversion's modifier can use more than one character. */ public interface ArgumentNameSpace { /** Returns all conversion's identifiers handled by this namespace * @return All conversion's modifiers */ public String[] symbols(); /** Return for a specified symbol (custom conversion's identifier) the standard conversion to use for formatting the underlying argument * @param symbol Custom conversion's identifier * @return Standard conversion's modifier to use for formatting the underlying argument */ public Conversion getConversion(String symbol); /** When an argument is a date-time field, a complementary date-time conversion is required. * This method return which separator must be used to separate a specified symbol from it's date-time conversion. * @return date-time separator to use with a specified symbol */ public String getDateSeparator(String symbol); /** Invoked before using a symbol by a pattern * @param symbol Symbol marked as used */ public void use(String symbol); } /** IndexableArgumentNameSpace is a namespace that can define which argument must be used for formatting a specified conversion's identifier. * This kind of namespace handle argument's choice and the pattern can't try to specify any index informations. */ public interface IndexableArgumentNameSpace extends ArgumentNameSpace { /** Returns which argument's must be used for formatting the specified symbol * @param symbol Symbol to format * @return index ArgumentDetails's index 0-based. */ public int index(String symbol); } /** AttributeArgumentNameSpace is a namespace binding conversion's identifier to a particular property of the argument. * This namespace get a method for retrieve the property value of an argument depending of the conversion's identifier. */ public interface AttributeArgumentNameSpace extends ArgumentNameSpace { /** Return the related property for the specified symbol and the specified argument * @param value argument to format * @param symbol Symbol describing which property must be formatted * @param locale Locale to use * @return the requested property */ public Object getAtttribute(Object value,String symbol,Locale locale); } /** This extended implementation of ArgumentNameSpace allow to control more informations on arguments like: *
    *
  • Flags that can modify the output format
  • *
  • Width that give the minium number of characters
  • *
  • Precision that restrict the number of characters (depend of the conversion)
  • *
*/ public interface DetailedArgumentNameSpace extends ArgumentNameSpace { /** Return details to use for a specified symbol * @param details Current details informations picked-up from the pattern specification * @param symbol Which symbol to apply the details * @return New details to use for this argument */ public ArgumentDetails getDetails(ArgumentDetails details, String symbol); } /** Describe all conversion types managed by this formatter */ public enum Conversion { DECIMAL_INTEGER('d', false, null , null ), OCTAL_INTEGER('o', false, null ,"0"), HEXADECIMAL_INTEGER('x', false, null,"0x"), HEXADECIMAL_INTEGER_UPPER('X', true, HEXADECIMAL_INTEGER,"0X"), SCIENTIFIC('e', false, null,null), SCIENTIFIC_UPPER('E', true, SCIENTIFIC,null), GENERAL('g', false, null,null), GENERAL_UPPER('G', true, GENERAL,null), DECIMAL_FLOAT('f', false, null,null), HEXADECIMAL_FLOAT('a', false, null,null), HEXADECIMAL_FLOAT_UPPER('A', true, HEXADECIMAL_FLOAT,null), CHARACTER('c', false, null,null), CHARACTER_UPPER('C', true, CHARACTER,null), DATE_TIME('t', false, null,null), DATE_TIME_UPPER('T', true, DATE_TIME,null), BOOLEAN('b', false, null,null), BOOLEAN_UPPER('B', true, BOOLEAN,null), STRING('s', false, null,null), STRING_UPPER('S', true, STRING,null), HASHCODE('h', false, null,null), HASHCODE_UPPER('H', true, HASHCODE,null); private char car; private boolean upper = false; private Conversion effective = null; private String alternate = null; private Conversion(char car, boolean upper, Conversion conversion, String alternate) { this.car = car; this.upper = upper; this.effective = conversion == null ? this : conversion; this.alternate = alternate; } /** Retrieve the conversion's modifier character * @return conversion's modifier character */ public char getChar() { return this.car; } /** Indicate if this conversion's modifier request an UPPERCASE formatting * @return true if this conversion's modifier request an UPPERCASE formatting */ public boolean isUpper() { return this.upper; } /** Return the effective conversion. Be exemple STRING_UPPER return STRING because it's the same conversion excepting the UPPER flag * @return effective conversion */ public Conversion getEffectiveConversion() { return this.effective; } /** Return the alternate prefix to use with this conversion if alternate flag is specified * @return alternate prefix */ public String getAlternate() { return this.alternate; } /** Parse a character and return the underlying conversion's enum * @param car Conversion's modifier character * @return Conversion's enum (return null if no correspondance found) */ public static Conversion parse(char car) { Conversion[] all = values(); for (Conversion c : all) { if (c.getChar() == car) { return c; } } return null; } }; /** Describe all date-time conversion's suffix modifiers */ public enum DateTimeConversion { HOUR_OF_DAY_0('H'), HOUR_0('I'), HOUR_OF_DAY('k'), HOUR('l'), MINUTE('M'), NANOSECOND('N'), MILLISECOND('L'), MILLISECOND_SINCE_EPOCH('Q'), AM_PM('p'), SECONDS_SINCE_EPOCH('s'), SECOND('S'), TIME('T'), ZONE_NUMERIC('z'), ZONE('Z'), NAME_OF_DAY_ABBREV('a'), NAME_OF_DAY('A'), NAME_OF_MONTH_ABBREV('b'), NAME_OF_MONTH('B'), CENTURY('C'), DAY_OF_MONTH_0('d'), DAY_OF_MONTH('e'), NAME_OF_MONTH_ABBREV_X('h'), DAY_OF_YEAR('j'), MONTH('m'), YEAR_2('y'), YEAR_4('Y'), TIME_12_HOUR('r'), TIME_24_HOUR('R'), DATE_TIME('c'), DATE('D'), ISO_STANDARD_DATE('F'); private char car; private DateTimeConversion(char car) { this.car = car; } /** Retrieve the date-time conversion's modifier character * @return date-time conversion's modifier character */ public char getChar() { return this.car; } /** Parse a character and return the underlying date-time conversion's enum * @param car Date-time conversion's modifier character * @return Date-time conversion's enum (return null if no correspondance found) */ public static DateTimeConversion parse(char car) { DateTimeConversion[] all = values(); for (DateTimeConversion c : all) { if (c.getChar() == car) { return c; } } return null; } }; /** Compiled pattern form. *

* Some usecase need to format the same pattern with differents argument's value. * In this case, compiling such pattern and reuse theses forms for the formatting process is more efficient. *

* This class represent a compiled-pattern with {@link PatternFormatter#compile} and can be reuse with {@link PatternFormatter#format} */ public static final class Pattern { private String pattern = null; // last-compiled pattern private List elements = null; // compiled structure private ArgumentNameSpace nameSpace = null; // namespace used /** Default constructor */ private Pattern(String pattern, ArgumentNameSpace nameSpace) { this.pattern = pattern; this.nameSpace = nameSpace; } /** Retrieve the source-pattern compiled to this form * @return Source pattern of this compiled form */ public String getPattern() { return this.pattern; } /** Retrieve the namespace used for this compiled-pattern * @return NameSpace used for this compiled-pattern */ public ArgumentNameSpace getNameSpace() { return this.nameSpace; } /** Get a compiled pattern's element */ private PatternElement getElements(int index) { if( this.elements == null ) return null; return this.elements.get(index); } /** Get numbers of compiled pattern's elements */ private int size() { if( this.elements == null ) return 0; return this.elements.size(); } /** Add a compiled pattern's element */ private void addElement(PatternElement element) { if( this.elements == null ) this.elements = new ArrayList(10); this.elements.add(element); } } /** Create an empty PatternFormatter */ public PatternFormatter() { } /** Compile a pattern that can be re-used for formatting string with the #toString(Object... args) method. * @param pattern pattern to compile */ public static Pattern compile(String pattern) { return compile(pattern, null); } /** Compile a pattern that can be re-used for formatting string with the #toString(Object... args) method. * @param pattern pattern to compile * @param namespace namespace to use */ public static Pattern compile(String pattern, ArgumentNameSpace namespace) { Pattern result = new Pattern(pattern,namespace); int start = 0; int end = 0; while (start < pattern.length()) { end = pattern.indexOf('%', start); if (end >= 0) { if (end > start) { /** We have a fixed-string before the current argument */ result.addElement( new StringElement(pattern.substring(start, end)) ); } /** Parse the argument with an ArgumentParser */ ArgumentParser ap = new ArgumentParser( result.getPattern() , end, result.getNameSpace() ); result.addElement( ap.parse() ); /** Move to the end of all bytes consumed by the argument parser */ start = ap.getEndArgumentPosition(); } else { /** No more argument, add the fixed-tailer of this pattern */ end = pattern.length(); if (end > start) { result.addElement( new StringElement(pattern.substring(start, end)) ); break; } } } return result; } /** Format the pattern with the given arguments and the default locale * This method compile the specified pattern if it is different from the last compiled one * * @param format Pattern to format * @param args arguments to use for format the pattern * * @return the pattern formatted */ public static String format(String format, Object... args) { return format(format, Locale.getDefault() , args ); } /** Format the pattern with the given arguments and the specified locale * This method compile the specified pattern if it is different from the last compiled one * * @param format Pattern to format * @param locale Locale to use * @param args arguments to use for format the pattern * * @return the pattern formatted */ public static String format(String format, Locale locale , Object... args) { return format( compile(format) , locale , args ); } /** Format a compiled-pattern with the given arguments and the default locale * This method don't compile any pattern and reuse a previously compiled one. * * @param pattern Compiled-Pattern to format * @param args arguments to use for format the pattern * * @return the pattern formatted */ public static String format(Pattern pattern, Object... args) { return format(pattern, Locale.getDefault() , args ); } /** Format a compiled-pattern with the given arguments and the specified locale * This method don't compile any pattern and reuse a previously compiled one. * * @param pattern Compiled-Pattern to format * @param locale Locale to use * @param args arguments to use for format the pattern * * @return the pattern formatted */ public static String format(Pattern pattern, Locale locale , Object... args) { if( locale == null ) locale = Locale.getDefault(); String[] array = new String[ pattern.size() ]; int arrayLength = 0; int last = -1; // last index used int lasto = -1; // last implicit index used (ordinary) for (int i = 0; i < pattern.size(); i++) { PatternElement pe = pattern.getElements(i); int index = pe.index(); try { switch (index) { case -2: // fixed string, "%n", or "%%" array[i] = pe.format( null , locale ); break; case -1: // relative index if (last < 0 || (args != null && last > args.length - 1)) { throw new MissingFormatArgumentException(pe.toString()); } array[i] = pe.format( (args == null ? null : args[last]) , locale ); break; case 0: // ordinary index lasto++; last = lasto; if (args != null && lasto > args.length - 1) { throw new MissingFormatArgumentException(pe.toString()); } array[i] = pe.format( (args == null ? null : args[lasto]) , locale ); break; default: // explicit index last = index - 1; if (args != null && last > args.length - 1) { throw new MissingFormatArgumentException(pe.toString()); } array[i] = pe.format( (args == null ? null : args[last]) , locale ); break; } arrayLength += (array[i] == null ? 0 : array[i].length()); } finally { } } /** Constructing result */ char[] result = new char[arrayLength]; int resultPos = 0; for (String e : array) { e.getChars(0, e.length(), result, resultPos); resultPos += e.length(); } return new String(result); } /** Details how an argument must be formatted with more informations *

    *
  • Flags that can modify the output format
  • *
  • Width that give the minium number of characters
  • *
  • Precision that restrict the number of characters (depend of the conversion)
  • *
*/ public static class ArgumentDetails { private Flags flags = Flags.NONE; private int width = -1; private int precision = -1; private DateTimeConversion timeConversion = null; /** Private constructor */ private ArgumentDetails(Flags flags,int width,int precision,DateTimeConversion datetimeConversion) { setFlags(flags); setWidth(width); setPrecision(precision); setDateTimeConversion(datetimeConversion); } /** Retrieve flags to use with the argument * @return FLags to use */ public Flags getFlags() { return this.flags; } /** Define a new flags set to use * @param flags new flag set to use */ public void setFlags(Flags flags) { this.flags = flags; } /** Retrieve the width to use (-1 if no width is specified) * @return Width to use (-1 if no one) */ public int getWidth() { return this.width; } /** Define the new width to use * @param width new width */ public void setWidth(int width) { this.width = width; } /** Retrieve the precision to use (--1 if no precision is specified) * @return Precision to use (-1 if no one) */ public int getPrecision() { return this.precision; } /** Define the new precision to use * @param precision new precision */ public void setPrecision(int precision) { this.precision = precision; } /** Retrieve the date-time complementary conversion to use when the argurment is a date-time * @return date-time complementary conversion to use when the argument is a date-time */ public DateTimeConversion getDateTimeConversion() { return this.timeConversion; } /** Define the new date-time complementaty conversion * @param dateTimeConversion new date-time complementary conversion */ public void setDateTimeConversion(DateTimeConversion dateTimeConversion) { this.timeConversion = dateTimeConversion; } } /** This class helps to parse an argument of the form %[argument_index$][flags][width][.precision]conversion * This class also interpret to special arguments: %n (line feed) and %% (character %) *

* conversiont part can be also replaced by an identifier from a specified arguments namespace *

* The parsing process return a PatternElement and manage all kinds of ArgumentNameSpace */ private static class ArgumentParser { /** Line separator for implementing %n */ static String lineSeparator = System.getProperty("line.separator"); /** Global members on the pattern */ private String pattern = null; // global pattern being to be parsed private int startArgumentPos = 0; // start index from the pattern of the argument to parse private int endArgumentPos = 0; // end index from the pattern of the argument parsed (set at the end of the parse process) private ArgumentNameSpace nameSpace = null; // namespace used for the conversion's modifier /** Members used for scan process */ private int phase = 0; // current phase of the scan // 0 = argument_index // 1 = flags // 2 = witdh // 3 = precision // 4 = conversion private int pos = 0; // current index of the character to parse for the current phase scan private int startPhasePos = 0; // start index from the pattern of the current phase scan /** Result members */ int argument_index = 0; String flags = null; int width = -1; int precision = -1; Conversion conversion = null; /** Constructor of the parser * @param pattern global pattern being parser * @param startArgumentPos Start index of the argument to parse from the pattern. This index must refer a '%' character * @param nameSpace NameSpace to use for identify arguments (maybe be null) */ public ArgumentParser(String pattern, int startArgumentPos, ArgumentNameSpace nameSpace) { this.pattern = pattern; this.startArgumentPos = startArgumentPos; this.nameSpace = nameSpace; } /** Return the ending position (exclusive) from the global pattern of the scanned argument. * The index represent the first character that is not take part of the scanned argument. *

* This method can be invoked only after the argument is parsed * * @return End of argument position */ public int getEndArgumentPosition() { return this.endArgumentPos; } /** Parse the argument of the form %[argument_index$][flags][width][.precision]conversion * This implementation works as follow: * - The scan is on a particular phase (starting to 0, ending by completing phase 4) * - Take a character, if the character is compatible for the current phase place it on a buffer and continue with the next character * - If the character is not compatible (called breaker character) with the current phase: * - Check if buffer's characters for the ended phase are legits, if true: the phase consume theses chars and replay the breaker character for the new phase * - If buffer's characters ARE NOT LEGITS then flush buffer and theses characters will be re-scan for the new phase. * - Buffer's characters can be not legits if the set don't respect phase format (exemple: phase 0 must end by a '$' and phase 3 must start by a '.') *

* This method treat also two specials argument's form: %n (line feed) and %% (isolated %) *

* Example: * - ArgumentDetails %20d * - Phase 0 , scan '2' : character compatible (digit) : hold this character that give a buffer "2" * - Phase 0 , scan '0' : character compatible (digit) : hold this character that give a buffer "20" * - Phase 0 , scan 'd' : character INCOMPATIBLE * - Check legit for buffer "20" for the phase 0 : buffer NOT LEGIT because phase 0 need to end by a '$' * - Change to phase 2, flush the buffer that will replay theses characters * - Phase 1 , scan '2' : character INCOMPATIBLE * - No buffer, change to phase 2 * - Phase 2 , scan '2' : character compatible (digit) : hold this character that give a buffer "2" * - Phase 2 , scan '0' : character compatible (digit) : hold this character that give a buffer "20" * - Phase 2 , scan 'd' : character INCOMPATIBLE * - Check legit for buffer "20" for the phase 2 : buffer LEGIT , consume buffer and get a WIDTH of 20 * - Change to phase 3, we will replay the 'd' but not '2' and '0' because they are consumed by the phase 2 * - Phase 3 , scan 'd' : character INCOMPATIBLE * - No buffer, change to phase 4 * - Phase 4 , scan 'd' : character compatible, this phase just peek one character and get the CONVERSION of d * - parsing END * * In this example, this method scan a set of 9 characters ('2' , '0' , 'd' , '2' , '2' , '0' , 'd' , 'd' , 'd') * * @return PatternElement describing this argument after parsing */ private PatternElement parse() { if (pattern.charAt(startArgumentPos) != '%') { throw new IllegalStateException("argument pattern must start by %"); } this.pos = startArgumentPos + 1; // skip % this.phase = 0; this.startPhasePos = pos; while (pos < pattern.length()) { if (phase == 4) { return getPatternElement(); } switch (pattern.charAt(pos)) { case '-': case '#': case '+': case ' ': // case '0' : // processed in default case since it is also a digit character case ',': case '(': if (phase == 1) { pos++; // continue scan break; } newPhase(); break; case '$': if( phase == 0 ) pos++; // the $ take part on the phase 0 newPhase(); break; case '.': if (phase == 3 && pos == startPhasePos) { pos++; // continue scan break; // it's the beggining of the phase 3 } newPhase(); break; case '<': if (phase == 0) { pos++; // continue scan break; // it's a part of the phase 0 } newPhase(); break; case '%': case 'n': if (phase == 0 && pos == (startArgumentPos + 1)) { this.endArgumentPos = Math.min(pattern.length(), pos + 1); if (pattern.charAt(pos) == '%') { return new StringElement("%"); } return new StringElement(lineSeparator); } default: if (Character.isDigit(pattern.charAt(pos))) { if (phase == 0 || phase == 2 || phase == 3) { pos++; // continue scan break; // part of theses phases } if (phase == 1 && pattern.charAt(pos) == '0') { pos++; // continue scan break; // part of this phase } } newPhase(); } } throw new IllegalStateException("unable to fin conversion properties on last argument"); } /** This method mark the end of the current phase and some characters have been buffered for this ending phase. * Theses characters will be consumed by the ending phase if they are legits. * If they aren't legits, theses characters will be flush to be re-scanned for the new phase. *

    *
  • phase 0 : characters are legits if they are ending by a '$'
  • *
  • phase 1 : characters are legits if they start by '-' or '#' or '+' or ' ' or '0' or ',' or '('
  • *
  • phase 2 : characters are legits if they start by a digit character
  • *
  • phase 3 : characters are legits if they start by '.'
  • *
  • phase 4 : not managed by this method but by #getPatternElement()
  • *
*

* In all case, this method start the next phase aand manage some stuff *

    *
  • */ private void newPhase() { int endPhasePos = pos; /** Flag for determine if the ending phase consums bufferised characters */ boolean consumed = false; /** Consum characters if possible */ switch(phase) { case 0 : { if (endPhasePos > startPhasePos && pattern.charAt(endPhasePos - 1) == '$') { if (nameSpace != null && nameSpace instanceof IndexableArgumentNameSpace ) { throw new IllegalStateException("can't specify an index ordering statement with an IndexArgumentNameSpace"); } String s = this.pattern.substring(startPhasePos, endPhasePos - 1); if (s.equals("<")) { this.argument_index = -1; // preceding index } else { this.argument_index = Integer.parseInt(s); } consumed = true; } else { this.argument_index = 0; // ordinary index } } break; case 1 : { if (endPhasePos > startPhasePos) { char firstChar = this.pattern.charAt(startPhasePos); if (firstChar == '-' || firstChar == '#' || firstChar == '+' || firstChar == ' ' || firstChar == '0' || firstChar == ',' || firstChar == '(') { this.flags = this.pattern.substring(startPhasePos, endPhasePos); consumed = true; } } } break; case 2 : { if (endPhasePos > startPhasePos) { char firstChar = this.pattern.charAt(startPhasePos); if (Character.isDigit(firstChar)) { String s = this.pattern.substring(startPhasePos, endPhasePos); this.width = Integer.parseInt(s); consumed = true; } } } break; case 3 : { if (endPhasePos > startPhasePos) { char firstChar = this.pattern.charAt(startPhasePos); if (firstChar == '.') { String s = this.pattern.substring(startPhasePos + 1, endPhasePos); this.precision = Integer.parseInt(s); consumed = true; } } } break; } /** Pass to the next phase */ phase++; if (consumed) startPhasePos = endPhasePos; else pos = startPhasePos; // replay from the last start point } /** This method is responsible to parse the conversion part of the argument pattern. * A conversion modifier take place in a single character that must match with the Conversion enumeration. *

    * In case of use of an argument's namespace, this method find the biggest custom symbol that will give: *

      *
    • The Conversion flag to use
    • *
    • The argument's index to use (specified index in the argument pattern is incompatible with a namespace)
    • *
    *

    * #getPatternElement() is then end of the scan process and return the PatternElement refleting the argument. * * @return PatternElement describing this argument after parsing */ private PatternElement getPatternElement() { int length = 1; DateTimeConversion dtc = null; String matchSymbol = null; if (nameSpace != null) { for (String symbol : nameSpace.symbols()) { if (pattern.startsWith(symbol, startPhasePos)) { if (matchSymbol == null || symbol.length() > matchSymbol.length()) { matchSymbol = symbol; length = symbol.length(); } } } if (matchSymbol != null) { nameSpace.use(matchSymbol); // mark this symbol "used" if( nameSpace instanceof IndexableArgumentNameSpace ) { this.argument_index = ((IndexableArgumentNameSpace)nameSpace).index( matchSymbol ) + 1; // namespace is 0-index but the property is 1-index } this.conversion = nameSpace.getConversion(matchSymbol); } if (this.conversion == null) { throw new IllegalArgumentException("unknown [" + pattern.substring(startPhasePos) + "] symbol's namespace"); } } else { this.conversion = Conversion.parse(pattern.charAt(startPhasePos)); if (conversion == null) { throw new IllegalArgumentException("unknown [" + pattern.charAt(startPhasePos) + "] conversion's flag"); } } /** If the conversion's modifier is a date-time, it must follow a suffix that determines how print this date */ switch(this.conversion) { case DATE_TIME : case DATE_TIME_UPPER : if( this.nameSpace != null ) { String datetimeSeparator = this.nameSpace.getDateSeparator( matchSymbol ); if( datetimeSeparator != null && datetimeSeparator.length() > 0 ) { if( ! pattern.startsWith( datetimeSeparator , startPhasePos + length ) ) { throw new IllegalArgumentException("date-time suffix from '" + matchSymbol + "' must be separate with '" + datetimeSeparator + "'"); } } length += datetimeSeparator == null ? 0 : datetimeSeparator.length(); } char charDTC = pattern.charAt( startPhasePos + (length++) ); dtc = DateTimeConversion.parse(charDTC); if( dtc == null ) throw new IllegalArgumentException("unknown [" + charDTC + "] Date-Time suffix"); default : break; } this.endArgumentPos = Math.min(pattern.length(), startPhasePos + length); ArgumentDetails details = new ArgumentDetails( Flags.parse( flags , conversion ) , width , precision , dtc); if( nameSpace instanceof DetailedArgumentNameSpace ) { details = ((DetailedArgumentNameSpace)nameSpace).getDetails( details , matchSymbol ); } ArgumentElement result = new ArgumentElement(argument_index, conversion , details ); if( nameSpace instanceof AttributeArgumentNameSpace ) { result.setNameSpace( (AttributeArgumentNameSpace)nameSpace , matchSymbol ); } return result; } } /** When a pattern is compiled, this pattern is represented by a sequence of PatternElement * Each PatternElement can format a fragment of the entire pattern depending on an argument. * * A PatternElement know which argument is needed for formatting itself with the help of the #index() method. * This method return the index inside the argument's array of the requested argument for this PatternElement *

      *
    • -2 : This PatternElement don't need any argument, it's probably a constant fragment like a String litteral
    • *
    • -1 : This PatternElement use the same argument than the previous PatternElement
    • *
    • 0 : This PatternElement use the next implicit argument on the array
    • *
    • x : This PatternElement use the specified index argument, this method is 1-based index and the array is 0-based index *
    *

    * After what, each PatternElement can format itself with the #format(Object arg,Locale locale) * For have the final result, we need to append each PatternElement result. */ private interface PatternElement { /** Donne l'index de l'argument � utiliser pour formatter cet �lement * @return Index de l'argument � utiliser pour formatter cet �l�ment * -2 : Aucun car il s'agit d'un �lement fixe * -1 : Prendre l'argument pr�cedent au dernier argument utilis� * 0 : Prochain argument * x : Index pr�cis */ public int index(); /** Mise en forme de l'�lement * @param arg ArgumentDetails � utiliser pour mettre en forme l'�lement */ public String format(Object arg , Locale locale ); } /** PatternElement implementation for holding a String-litteral fragment of the compiled pattern */ private static class StringElement implements PatternElement { /** String-litteral */ private String s = null; /** Constructor */ public StringElement(String s) { this.s = s; } /** Formatting the literral return simply the */ public String format(Object arg , Locale locale ) { return s; } /** -2 car n'a pas besoin d'argument */ public int index() { return -2; } } /** PatternElement holding an argument configuration and formatting implementation */ private static class ArgumentElement implements PatternElement { /** configuration members */ int argument_index = 0; ArgumentDetails argument_details = null; /** Conversion's attributes */ Conversion conversion = null; /** Namespace's attributes */ AttributeArgumentNameSpace nameSpace = null; String nameSpaceSymbol = null; /** DecimalFormatSymbols * And some related informations depending on the local used for formatting numbers */ Locale loc = null; DecimalFormatSymbols dfs = null; char groupSeparator = ' '; char decimalSeparator = '.'; char zero = '0'; char[] buffer = null; // used for decimal formatting /** Some members for formatting dates */ Calendar calendar = null; DateFormatSymbols datefs = null; /** Construct an anrgument formatter that hold all properties needed for formatting it */ public ArgumentElement(int index, Conversion conversion , ArgumentDetails details ) { this.argument_index = index; this.argument_details = details; this.conversion = conversion.getEffectiveConversion(); /** Init the buffer for decimal formatting */ switch( this.conversion ) { case DECIMAL_FLOAT : case GENERAL : case SCIENTIFIC : buffer = new char[100]; // too complex to determine which size we need // 100 seems to be big enough break; case DATE_TIME : calendar = Calendar.getInstance(); break; default : break; } } /** Define a AttributeArgumentNameSpace to use for formatting this argument * @param nameSpace AttributeArgumentNameSpace to use for formatting this argument * @param symbol Namespace's symbol used with this argument */ public void setNameSpace( AttributeArgumentNameSpace nameSpace , String symbol ) { this.nameSpace = nameSpace; this.nameSpaceSymbol = symbol; } /** Index de l'argument � utiliser */ public int index() { return argument_index; } /** Return flags to use */ public Flags getFlags() { return argument_details.getFlags(); } /** return Width to use */ public int getWidth() { return argument_details.getWidth(); } /** Return Precision to use */ public int getPrecision() { return argument_details.getPrecision(); } /** return DateTimeConversion to use */ public DateTimeConversion getDateTimeConversion() { return argument_details.getDateTimeConversion(); } /** Formatting the argument * Depending on the conversion modifier, the right formatting implementation will be invoked */ public String format(Object arg , Locale locale ) { /** deffer the value from the namespace */ if( nameSpace != null ) { arg = nameSpace.getAtttribute( arg , nameSpaceSymbol , locale ); } switch (this.conversion) { case DATE_TIME : return formatDateTime(arg , locale ); case DECIMAL_INTEGER : case OCTAL_INTEGER : case HEXADECIMAL_INTEGER : return formatInteger(arg , locale ); case SCIENTIFIC : case GENERAL : case DECIMAL_FLOAT : case HEXADECIMAL_FLOAT : return formatFloat(arg , locale ); case CHARACTER : return formatCharacter(arg , locale ); case BOOLEAN : return formatBoolean(arg , locale ); case STRING : return formatString(arg , locale ); case HASHCODE : return formatHashCode(arg , locale ); default : return ""; } } /** Format an integer, * @param arg ArgumentDetails to format, this must be a Number instance in order to formatting the argument * @param locale Localization to use for formatting the number (used for group separator, decimal separator, ...) * @return number format */ private String formatInteger(Object arg , Locale locale ) { if (arg == null) return "null"; String result = null; if( arg instanceof Number ) { long value = ((Number)arg).longValue(); if( value < 0 && ( conversion == Conversion.HEXADECIMAL_INTEGER || conversion == Conversion.OCTAL_INTEGER ) ) { /** HEXADECIMAL and OCTAL hasn't negative number and represent absolute bits. * * As exemple, a byte value has 8 bits and have theses representation in binary * Max value +127 == 01111111 * .... * value +1 == 00000001 * value 0 == 00000000 * value -1 == 11111111 (substract 1 to 0 and you wrap to the end of bits * .... * Min value -128 == 10000000 * * If you convert a byte decimal value (8 bits) into a short decimal value (16 bits) * the -128 decimal representation in 8 bits become +128 decimal representation in 16 bits. * * But when JAVA cast a byte to short decimal value, the negative number is adapted to the target and the negative bit is moved and the original bits representation is broken * In this case, we must restore the original bits representation. * * The original bits representation can be restored by adding 256 (2^8) in the case of a source value in 8 bits (byte) * We must add 2^(size_in_bits_of_source_value) that can be done by adding (1 << size_in_bits_of_source_value) */ int bitSize = 0; if( arg instanceof Integer ) bitSize = Integer.SIZE; else if( arg instanceof Short ) bitSize = Short.SIZE; else if( arg instanceof Byte ) bitSize = Byte.SIZE; value += (1 << bitSize ); } result = formatInteger(value,locale); } if( result == null ) fail(arg); return justify(result); } /** Format a float */ private String formatFloat(Object arg , Locale locale ) { if (arg == null) return "null"; String result = null; if( arg instanceof Number ) { result = formatFloat( ((Number)arg).doubleValue() , locale ); } if( result == null ) fail(arg); return justify(result); } /** Format a character */ private String formatCharacter(Object arg , Locale locale ) { if( arg == null ) return "null"; String result = null; if( arg instanceof Character ) { result = ((Character)arg).toString(); } else { if( arg instanceof Byte ) { byte b = ((Byte)arg).byteValue(); if( Character.isValidCodePoint(b) ) result = new String( Character.toChars( b ) ); else throw new IllegalFormatCodePointException(b); } else if( arg instanceof Short ) { short s = ((Short)arg).shortValue(); if( Character.isValidCodePoint(s) ) result = new String( Character.toChars( s ) ); else throw new IllegalFormatCodePointException(s); } else if( arg instanceof Integer ) { int i = ((Integer)arg).intValue(); if( Character.isValidCodePoint(i) ) result = new String( Character.toChars( i ) ); else throw new IllegalFormatCodePointException(i); } } if( result == null ) fail(arg); return formatString(result,locale); } /** Format a boolean * If the argument is not a Boolean or boolean types, aby not-null argument will return true, a null argument will return false * @return Boolean formatted */ private String formatBoolean(Object arg, Locale locale) { String result = null; if( arg == null ) result = Boolean.FALSE.toString(); else { if (arg instanceof Boolean) { result = formatString( ((Boolean) arg).toString() , locale ); } else result = Boolean.TRUE.toString(); } return formatString(result,locale); } /** Format a date-time */ private String formatDateTime(Object arg , Locale locale ) { if( arg == null ) return "null"; Calendar date = null; if( arg instanceof Number ) { date = this.calendar; date.setTimeInMillis( ((Number)arg).longValue() ); } else if( arg instanceof Date ) { date = this.calendar; date.setTime( (Date)arg ); } else if( arg instanceof Calendar ) { date = (Calendar)arg; } if( date == null ) fail(arg); return formatDateTime( date , getDateTimeConversion() , locale ); } /** Format a date-time */ private String formatDateTime( Calendar date , DateTimeConversion c , Locale locale ) { switch( c ) { case HOUR_OF_DAY_0: int hour = date.get(Calendar.HOUR_OF_DAY); return formatLocalizedNumber( Integer.toString(hour), false , 2 , Flags.ZERO_PAD , locale ); case HOUR_0: hour = date.get(Calendar.HOUR) + 1; return formatLocalizedNumber( Integer.toString(hour), false , 2 , Flags.ZERO_PAD , locale ); case HOUR_OF_DAY: hour = date.get(Calendar.HOUR_OF_DAY); return Integer.toString(hour); case HOUR: hour = date.get(Calendar.HOUR) + 1; return Integer.toString(hour); case MINUTE: int minute = date.get(Calendar.MINUTE); return formatLocalizedNumber( Integer.toString(minute), false , 2 , Flags.ZERO_PAD , locale ); case NANOSECOND: int nano = date.get(Calendar.MILLISECOND) * 1000000; return formatLocalizedNumber( Integer.toString(nano), false , 9 , Flags.ZERO_PAD , locale ); case MILLISECOND: int milli = date.get(Calendar.MILLISECOND); return formatLocalizedNumber( Integer.toString(milli), false , 3 , Flags.ZERO_PAD , locale ); case MILLISECOND_SINCE_EPOCH: long millis = date.getTimeInMillis(); return formatLocalizedNumber( Long.toString(millis), false , locale ); case AM_PM: DateFormatSymbols datefs = getDateFormatSymbols(locale); String s = justify( datefs.getAmPmStrings()[ date.get(Calendar.AM_PM)] ); if( getFlags().contains(Flags.UPPER) ) return s.toUpperCase(locale); return s; case SECONDS_SINCE_EPOCH: long seconds = date.getTimeInMillis() / 1000; return formatLocalizedNumber( Long.toString(seconds), false , locale ); case SECOND: int second = date.get( Calendar.SECOND ); return formatLocalizedNumber( Integer.toString(second), false , 2 , Flags.ZERO_PAD , locale ); case TIME: // %tH:%tM:%tS String sHour = formatDateTime(date , DateTimeConversion.HOUR_OF_DAY_0 , locale); String sMinute = formatDateTime(date , DateTimeConversion.MINUTE , locale); String sSeconde = formatDateTime(date , DateTimeConversion.SECOND , locale); char[] result = new char[ 8 ]; sHour.getChars( 0 , 2 , result , 0 ); result[2] = ':'; sMinute.getChars( 0 , 2 , result , 3 ); result[5] = ':'; sSeconde.getChars( 0 , 2 , result , 6 ); return new String(result); case ZONE_NUMERIC: int i = date.get( Calendar.ZONE_OFFSET ); String prefix = i < 0 ? "-" : "+"; i = Math.abs(i); int min = i / 60000; int offset = (min / 60) * 100 + (min % 60); return formatLocalizedNumber( Integer.toString(offset) , false , 4 , Flags.ZERO_PAD , prefix , null , locale ); case ZONE: TimeZone tz = date.getTimeZone(); return tz.getDisplayName( date.get( Calendar.DST_OFFSET ) != 0 , TimeZone.SHORT , locale ); case NAME_OF_DAY_ABBREV: datefs = getDateFormatSymbols(locale); s = justify( datefs.getShortWeekdays()[ date.get( Calendar.DAY_OF_WEEK) ] ); if( getFlags().contains(Flags.UPPER) ) return s.toUpperCase(locale); return s; case NAME_OF_DAY: datefs = getDateFormatSymbols(locale); s = justify( datefs.getWeekdays()[ date.get( Calendar.DAY_OF_WEEK) ] ); if( getFlags().contains(Flags.UPPER) ) return s.toUpperCase(locale); return s; case NAME_OF_MONTH_ABBREV_X: case NAME_OF_MONTH_ABBREV: datefs = getDateFormatSymbols(locale); s = justify( datefs.getShortMonths()[ date.get( Calendar.MONTH) ] ); if( getFlags().contains(Flags.UPPER) ) return s.toUpperCase(locale); return s; case NAME_OF_MONTH: datefs = getDateFormatSymbols(locale); s = justify( datefs.getMonths()[ date.get( Calendar.MONTH) ] ); if( getFlags().contains(Flags.UPPER) ) return s.toUpperCase(locale); return s; case CENTURY: int century = date.get( Calendar.YEAR ) / 100; return formatLocalizedNumber( Integer.toString(century), false , 2 , Flags.ZERO_PAD , locale ); case DAY_OF_MONTH_0: int day = date.get( Calendar.DAY_OF_MONTH ); return formatLocalizedNumber( Integer.toString(day), false , 2 , Flags.ZERO_PAD , locale ); case DAY_OF_MONTH: day = date.get( Calendar.DAY_OF_MONTH ); return Integer.toString(day); case DAY_OF_YEAR: day = date.get( Calendar.DAY_OF_YEAR ); return formatLocalizedNumber( Integer.toString(day), false , 3 , Flags.ZERO_PAD , locale ); case MONTH: int month = date.get( Calendar.MONTH ) + 1; return formatLocalizedNumber( Integer.toString(month), false , 2 , Flags.ZERO_PAD , locale ); case YEAR_2: int year = date.get( Calendar.YEAR ); return formatLocalizedNumber( Integer.toString(year), false , 4 , Flags.ZERO_PAD , locale ).substring(2); case YEAR_4: year = date.get( Calendar.YEAR ); return formatLocalizedNumber( Integer.toString(year), false , 4 , Flags.ZERO_PAD , locale ); case TIME_12_HOUR: // %tI:%tM %tS %tp sHour = formatDateTime(date , DateTimeConversion.HOUR_0 , locale); sMinute = formatDateTime(date , DateTimeConversion.MINUTE , locale); sSeconde = formatDateTime(date , DateTimeConversion.SECOND , locale); String sAP_AM = formatDateTime(date , DateTimeConversion.AM_PM , locale); result = new char[ sHour.length() + sMinute.length() + sSeconde.length() + sAP_AM.length() + 3 ]; offset = 0; sHour.getChars(0 , sHour.length() , result , offset ); offset += sHour.length(); result[offset++] = ':'; sMinute.getChars(0 , sMinute.length() , result , offset ); offset += sMinute.length(); result[offset++] = ' '; sSeconde.getChars(0 , sSeconde.length() , result , offset ); offset += sSeconde.length(); result[offset++] = ' '; sAP_AM.getChars(0 , sAP_AM.length() , result , offset ); offset += sAP_AM.length(); return new String(result); case TIME_24_HOUR: // %tH:%tM sHour = formatDateTime(date , DateTimeConversion.HOUR_OF_DAY_0 , locale); sMinute = formatDateTime(date , DateTimeConversion.MINUTE , locale); result = new char[ 5 ]; sHour.getChars( 0 , 2 , result , 0 ); result[2] = ':'; sMinute.getChars( 0 , 2 , result , 3 ); return new String(result); case DATE_TIME: // %ta %tb %td %tT %tZ %tY (Sat Nov 04 12:02:33 EST 1999) String sNameDay = formatDateTime(date , DateTimeConversion.NAME_OF_DAY_ABBREV , locale); String sNameMonth = formatDateTime(date , DateTimeConversion.NAME_OF_MONTH_ABBREV , locale); String sDay = formatDateTime(date , DateTimeConversion.DAY_OF_MONTH_0 , locale); String sTime = formatDateTime(date , DateTimeConversion.TIME , locale); String sZone = formatDateTime(date , DateTimeConversion.ZONE , locale); String sYear = formatDateTime(date , DateTimeConversion.YEAR_4 , locale); result = new char[ sNameDay.length() + sNameMonth.length() + sDay.length() + sTime.length() + sZone.length() + sYear.length() + 5 ]; offset = 0; sNameDay.getChars(0 , sNameDay.length() , result , offset ); offset += sNameDay.length(); result[offset++] = ' '; sNameMonth.getChars(0 , sNameMonth.length() , result , offset ); offset += sNameMonth.length(); result[offset++] = ' '; sDay.getChars(0 , sDay.length() , result , offset ); offset += sDay.length(); result[offset++] = ' '; sTime.getChars(0 , sTime.length() , result , offset ); offset += sTime.length(); result[offset++] = ' '; sZone.getChars(0 , sZone.length() , result , offset ); offset += sZone.length(); result[offset++] = ' '; sYear.getChars(0 , sYear.length() , result , offset ); offset += sYear.length(); return new String(result); case DATE: // %tm/%td/%ty String sMonth = formatDateTime(date , DateTimeConversion.MONTH , locale); sDay = formatDateTime(date , DateTimeConversion.DAY_OF_MONTH_0 , locale); sYear = formatDateTime(date , DateTimeConversion.YEAR_2 , locale); result = new char[8]; sMonth.getChars(0 , 2 , result , 0 ); result[2] = '/'; sDay.getChars(0 , 2 , result , 3 ); result[5] = '/'; sYear.getChars(0 , 2 , result , 6 ); return new String(result); case ISO_STANDARD_DATE: // %tY-%tm-%td sYear = formatDateTime(date , DateTimeConversion.YEAR_4 , locale); sMonth = formatDateTime(date , DateTimeConversion.MONTH , locale); sDay = formatDateTime(date , DateTimeConversion.DAY_OF_MONTH_0 , locale); result = new char[10]; sYear.getChars(0 , 4 , result , 0 ); result[4] = '-'; sMonth.getChars(0 , 2 , result , 5 ); result[7] = '-'; sDay.getChars(0 , 2 , result , 8 ); return new String(result); default : fail(date); return null; } } /** Format a String, Formattable or any other objects */ private String formatString(Object arg , Locale locale ) { if( arg == null ) return "null"; if( arg instanceof Formattable ) { Formattable f = (Formattable)arg; Formatter formatter = new Formatter(); f.formatTo( formatter , getFlags().getValue() , getWidth(), getPrecision() ); return formatter.toString(); } return formatString( arg.toString() , locale ); } /** Format String */ private String formatString(String s , Locale locale ) { if ( getPrecision() != -1 && getPrecision() < s.length()) { s = s.substring(0, getPrecision() ); // truncate } if( getFlags().contains(Flags.UPPER) ) { s = s.toUpperCase(locale); } return justify(s); } /** Mise en forme d'un hascode */ private String formatHashCode(Object arg , Locale locale ) { return formatString( Integer.toHexString(arg.hashCode() ) , locale ); } /** Justification */ private String justify(String s) { if ( getWidth() == -1) { return s; } if (s.length() > getWidth() ) { return s; } int lengthJustification = getWidth() - s.length(); int offsetJustification = ( getFlags().contains( Flags.LEFT_JUSTIFY ) ? s.length() : 0 ); int offsetString = ( getFlags().contains( Flags.LEFT_JUSTIFY ) ? 0 : lengthJustification ); int endJustification = offsetJustification + lengthJustification; char[] result = new char[getWidth()]; for (int i = offsetJustification; i < endJustification; i++) { result[i] = ' '; } s.getChars(0, s.length(), result, offsetString); return new String(result); } private String formatInteger(long value , Locale locale ) { String result = null; // intermediate and final result char[] array = null; // array representing the final result int length = 0; // length of the array switch(conversion) { case DECIMAL_INTEGER : String prefix = null; String suffix = null; if( value < 0 ) { if( getFlags().contains( Flags.PARENTHESE ) ) { prefix = "("; suffix = ")"; } else prefix = "-"; } else { if( getFlags().contains( Flags.PLUS ) ) prefix = "+"; else if( getFlags().contains( Flags.LEADING ) ) prefix = " "; } result = formatLocalizedNumber( Long.toString(value) , value < 0 , prefix , suffix , locale ); // localize the raw string break; case HEXADECIMAL_INTEGER : result = Long.toHexString(value); case OCTAL_INTEGER : if( result == null ) result = Long.toOctalString(value); length = ( getFlags().contains( Flags.ZERO_PAD ) ? ( result.length() > getWidth() ? result.length() : getWidth() ) : result.length() ) + (getFlags().contains( Flags.ALTERNATE ) ? conversion.getAlternate().length() : 0 ); array = new char[length]; int pos = 0; if( getFlags().contains( Flags.ALTERNATE ) ) { String alt = conversion.getAlternate(); alt.getChars( 0 , alt.length() , array , pos ); pos += alt.length(); } if( getFlags().contains( Flags.ZERO_PAD ) && ( result.length() < getWidth() ) ) { int endPaddingPosition = ( getWidth() - result.length() ) + pos; for(int i = pos ; i < endPaddingPosition ; i++ ) { array[i] = '0'; } pos = endPaddingPosition; } result.getChars( 0 , result.length() , array , pos ); result = new String(array); break; default : fail(value); } if( result == null ) fail(value); return result; } private String formatFloat(double value , Locale locale ) { if( Double.isNaN(value) ) return getFlags().contains( Flags.UPPER ) ? "NAN" : "NaN"; if( Double.isInfinite(value) ) return getFlags().contains( Flags.UPPER ) ? "INFINITY" : "Infinity"; boolean negative = (Double.compare(value, 0.0) < 0); FormattedFloatingDecimal ffd = null; int precision = this.getPrecision() == -1 ? 6 : this.getPrecision(); int length = 0; switch( conversion ) { case DECIMAL_FLOAT : ffd = new FormattedFloatingDecimal(value, precision , FormattedFloatingDecimal.Form.DECIMAL_FLOAT); length = ffd.getChars(buffer); case SCIENTIFIC : ffd = new FormattedFloatingDecimal(value, precision, FormattedFloatingDecimal.Form.SCIENTIFIC); length = ffd.getChars(buffer); case GENERAL : if( precision == 0 ) precision = 1; ffd = new FormattedFloatingDecimal(value, precision, FormattedFloatingDecimal.Form.GENERAL); length = ffd.getChars(buffer); default : fail(value); } return null; } /** Return the DecimalFormatSymbols to use for the specified locale * @param locale Specified locale * @return DecimalFormatSymbols to use */ private DecimalFormatSymbols getDecimalFormatSymbols(Locale locale) { if( locale != loc || dfs == null ) { dfs = new DecimalFormatSymbols(locale); groupSeparator = dfs.getGroupingSeparator(); decimalSeparator = dfs.getDecimalSeparator(); zero = dfs.getZeroDigit(); loc = locale; } return dfs; } /** Return the DateFormatSymbols to use for the specified locale * @param locale Specified locale * @return DateFormatSymbols to use */ private DateFormatSymbols getDateFormatSymbols(Locale locale) { if( locale != loc || datefs == null ) { datefs = new DateFormatSymbols(locale); } return datefs; } /** Formatting a besically string created bye an Long.toString(...) methods into a localized form (with correct separators, zero and groups) * This method manage also the zero-padding and all flags from the ArgumentDetails configuration. * * @param basicString String to localize, this string must represent a number in a Java Standard form like Integer.toString(), Float.toString(), etc... * @param negative truefor localize a negative number * @param locale Locale to use for determines separators, zero...) * * @return localized number as a String */ private String formatLocalizedNumber(String basicString , boolean negative , Locale locale) { return formatLocalizedNumber(basicString, negative, null , null , locale); } /** Formatting a besically string created bye an Long.toString(...) methods into a localized form (with correct separators, zero and groups) * This method manage also the zero-padding and all flags from the ArgumentDetails configuration. * * @param basicString String to localize, this string must represent a number in a Java Standard form like Integer.toString(), Float.toString(), etc... * @param negative truefor localize a negative number * @param prefix optionnal prefix to add before the number (may be null) * @param suffix optionnal suffix to aff after the number (may be null) * @param locale Locale to use for determines separators, zero...) * * @return localized number as a String */ private String formatLocalizedNumber(String basicString , boolean negative , String prefix , String suffix , Locale locale) { return formatLocalizedNumber(basicString , negative , this.getWidth() , this.getFlags() , prefix , suffix , locale); } /** Formatting a besically string created bye an Long.toString(...) methods into a localized form (with correct separators, zero and groups) * * @param basicString String to localize, this string must represent a number in a Java Standard form like Integer.toString(), Float.toString(), etc... * @param negative truefor localize a negative number * @param width Width to use for formatting (it's the minimum size in characters of the number) * @param groupFlags true for enabling group separators * @param zeroPadFlags true for enabling zero padding for having the width size * @param locale Locale to use for determines separators, zero...) * * @return localized number as a String */ private String formatLocalizedNumber( String basicString , boolean negative , int width , Flags myFlags , Locale locale) { return formatLocalizedNumber(basicString , negative , width , myFlags , null , null , locale); } /** Formatting a besically string created bye an Long.toString(...) methods into a localized form (with correct separators, zero and groups) * * @param basicString String to localize, this string must represent a number in a Java Standard form like Integer.toString(), Float.toString(), etc... * @param negative truefor localize a negative number * @param width Width to use for formatting (it's the minimum size in characters of the number) * @param groupFlags true for enabling group separators * @param zeroPadFlags true for enabling zero padding for having the width size * @param prefix optionnal prefix to add before the number (may be null) * @param suffix optionnal suffix to aff after the number (may be null) * @param locale Locale to use for determines separators, zero...) * * @return localized number as a String */ private String formatLocalizedNumber( String basicString , boolean negative , int width , Flags myFlags , String prefix , String suffix , Locale locale) { /** Configure dfs */ getDecimalFormatSymbols(locale); int dotPosition = basicString.length(); // dot position on the basicString /** Computing the buffer to allocate depending on the basic string length and transformation to process */ int length = basicString.length(); // original length if( myFlags.contains( Flags.GROUP ) ) { /** check where is the 'dot'. The position will help us to find when add groups separator */ for(int i = 0 ; i < basicString.length() ; i++ ) { if( basicString.charAt(i) == '.' ) { dotPosition = i; break; } } length += ( dotPosition / 3 ); // place holder for groups separators (3 == groupingSize) } length += ( prefix == null ? 0 : prefix.length() ) + ( suffix == null ? 0 : suffix.length() ); // include prefix+suffix for padding int arrayPosition = 0; // index where we can insert characters result in the buffer array int padLength = 0; if( myFlags.contains( Flags.ZERO_PAD ) && width > length ) { padLength = (width - length); length = width; } /** Allocate the array and fill with the zero padding before starting */ char[] array = new char[ length ]; if( prefix != null ) { prefix.getChars( 0 , prefix.length() , array , 0 ); arrayPosition += prefix.length(); } if( myFlags.contains( Flags.ZERO_PAD ) ) { int endPos = arrayPosition + padLength; for( ; arrayPosition < endPos ; arrayPosition++ ) { array[arrayPosition] = zero; } } boolean decimalPart = false; for( int i = (negative ? 1 : 0 ) ; i < basicString.length() ; i++ ) { char source = basicString.charAt(i); char dest = source; if( source == '.' ) { dest = decimalSeparator; decimalPart = true; } else { dest = (char)( ( source - '0') + zero ); // slice characters depending on the "zero" digit to use } array[arrayPosition++] = dest; if( myFlags.contains( Flags.GROUP ) && !decimalPart && i < (dotPosition - 1) && ( (dotPosition - i) % 3 ) == 1 ) { array[arrayPosition++] = groupSeparator; } } if( suffix != null ) { suffix.getChars( 0 , suffix.length() , array , arrayPosition ); } /** result it */ return new String(array,0,arrayPosition); } /** Throw an IllegalFormatConversionException because the formatting fail */ private void fail(Object arg) { if( arg == null ) throw new IllegalFormatConversionException( conversion.getChar() , null ); throw new IllegalFormatConversionException( conversion.getChar() , arg.getClass() ); } } /** Represent flags set usable for an ArgumentDetails * This class is compatible with the java.util.Formatter$Flags class in bit values and can be used with java.util.Formattable objects *

    * Flags can't be instanciate directly. To create a flags set, you have two way: *

      *
    • Parse one from their characters with Flags.parse(String s)
    • *
    • Compose a flags set from standard ones with Flags.and(Flags)
    • *
    *
         *   Flags flagsA = Flags.parse("+,");
         *   Flags flagsB = Flags.PLUS.and( Flags.GROUP );
         * 
    */ public static class Flags { /** no flags */ public static final Flags NONE = new Flags(0); /** Flag '-' : Left justiciation */ public static final Flags LEFT_JUSTIFY = new Flags(1); /** Shadow flag (inheritance from the conversion's modifier) : Upper Case */ public static final Flags UPPER = new Flags(2); /** Flag '#' : Alternate prefix like 0x for hexadecimal */ public static final Flags ALTERNATE = new Flags(4); /** Flag '+' : Plus prefix for positive numbers */ public static final Flags PLUS = new Flags(8); /** Flag ' ' : Space prefix for positive numbers */ public static final Flags LEADING = new Flags(16); /** Flag '0' : Zero padding for numbers */ public static final Flags ZERO_PAD = new Flags(32); /** Flag ',' : Group padding for numbers */ public static final Flags GROUP = new Flags(64); /** Flag '(" : Parentheses enclosing negative numbers */ public static final Flags PARENTHESE = new Flags(128); /** current flags set */ private int value = 0; /** Indicate if this flags set is mutable or not */ private boolean mutable = false; /** Create an unmutable flags set (for constants) */ private Flags(int v) { this(v,false); } /** Create a mutable flags set (for parse and alterate flags set) */ private Flags(int v , boolean mutable) { this.value = v; this.mutable = mutable; } /** Return the current value of the flags set */ public int getValue() { return this.value; } /** Indicate if this flags set contains another flags set */ public boolean contains( Flags flag ) { return ( getValue() & flag.getValue() ) == flag.getValue(); } /** Add flags */ public Flags and( Flags flags ) { if( ! mutable ) { return new Flags( getValue() | flags.getValue() , true ); } this.value |= flags.getValue(); return this; } /** Remove flags */ public Flags remove( Flags flags ) { if( ! mutable ) { return new Flags( getValue() & ~flags.getValue() , true ); } this.value &= ~flags.getValue(); return this; } /** Parse a String litteral in a flags set */ public static Flags parse( String flags , Conversion conversion ) { if (flags == null) { return Flags.NONE; } Flags result = Flags.NONE; for (int i = 0; i < flags.length(); i++) { char c = flags.charAt(i); switch (c) { case '-': result = result.and(Flags.LEFT_JUSTIFY); break; case '#': result = result.and(Flags.ALTERNATE); break; case '+': result = result.and(Flags.PLUS); break; case ' ': result = result.and(Flags.LEADING); break; case '0': result = result.and(Flags.ZERO_PAD); break; case ',': result = result.and(Flags.GROUP); break; case '(': result = result.and(Flags.ALTERNATE); break; default: break; } } if( conversion.isUpper() ) { result = result.and( Flags.UPPER ); } return result; } } /** Default implementation of ArgumentNameSpace accepting standard symbols for all supported conversions. * This class is intended to use with CompositeArgumentNameSpace for creating namespace implementations supporting default conversion's modifier. */ public static class DefaultArgumentNameSpace implements ArgumentNameSpace { /** Returns all conversion's identifiers handled by this namespace * @return All conversion's modifiers */ public String[] symbols() { Conversion[] c = Conversion.values(); String[] result = new String[ c.length ]; for(int i = 0 ; i < c.length ; i++ ) { result[i] = ((Character)c[i].getChar()).toString(); } return result; } /** Return for a specified symbol (custom conversion's identifier) the standard conversion to use for formatting the underlying argument * @param symbol Custom conversion's identifier * @return Standard conversion's modifier to use for formatting the underlying argument */ public Conversion getConversion(String symbol) { return Conversion.parse( symbol.charAt(0) ); } /** Invoked before using a symbol by a pattern * @param symbol Symbol marked as used */ public void use(String symbol) { /** do nothing */ } /** When an argument is a date, a complementary date-time conversion is required. * This method return which separator must be used to separate the symbol from the date-time conversion. * @return date-time separator */ public String getDateSeparator(String symbol) { return ""; } } /** This namespace implementation allow to compose a namespace from differents one. * Each symbol's management is delegate to it's owner namespace. *

    * This composite can't use any IndexableArgumentNameSpace because they are not designed for it with their absolute indexing feature. * But AttributeArgumentNameSpace, DetailedArgumentNameSpace or standard ArgumentNameSpace can be mixed. */ public static class CompositeArgumentNameSpace implements AttributeArgumentNameSpace , DetailedArgumentNameSpace { private String[] symbols = null; private Map namespaces = null; public CompositeArgumentNameSpace(ArgumentNameSpace... ns) { this.symbols = mergeSymbols(ns); } /** This method merge all symbols from all namespace into a single array. * @param ns All namespace * @return an array with all symbols merged * @throws IllegalArgumentException if we encounteer conflicting symbols or if a namespace implements IndexableArgumentNameSpace */ private String[] mergeSymbols(ArgumentNameSpace... ns) { namespaces = new HashMap(); String[] result = null; int resultLength = 0; int newResultLength = 0; for(ArgumentNameSpace namespace : ns ) { if( namespace instanceof IndexableArgumentNameSpace ) throw new IllegalArgumentException("can't compose a namespace from an IndexArgumentNameSpace"); String[] toAdd = namespace.symbols(); resultLength = ( result != null ? result.length : 0 ); newResultLength = resultLength + toAdd.length; String[] newResult = new String[ newResultLength ]; if( result != null ) System.arraycopy(result , 0 , newResult , 0 , result.length ); System.arraycopy(toAdd , 0 , newResult , resultLength , toAdd.length ); /** swap */ result = newResult; for(String symbol : toAdd ) { if( namespaces.containsKey(symbol) ) throw new IllegalArgumentException("Conflicting symbol '" + symbol + "' on " + this ); namespaces.put(symbol,namespace); } } return result; } /** Returns all conversion's identifiers handled by this namespace * @return All conversion's modifiers */ public String[] symbols() { return symbols; } /** Return for a specified symbol (custom conversion's identifier) the standard conversion to use for formatting the underlying argument * @param symbol Custom conversion's identifier * @return Standard conversion's modifier to use for formatting the underlying argument */ public Conversion getConversion(String symbol) { ArgumentNameSpace ns = this.namespaces.get(symbol); if( ns == null ) return Conversion.STRING; return ns.getConversion(symbol); } /** Return the related property for the specified symbol and the specified argument * @param value argument to format * @param symbol Symbol describing which property must be formatted * @return the requested property */ public Object getAtttribute(Object value,String symbol,Locale locale) { ArgumentNameSpace ns = this.namespaces.get(symbol); if( ! (ns instanceof AttributeArgumentNameSpace ) ) return value; return ((AttributeArgumentNameSpace)ns).getAtttribute(value,symbol,locale); } /** When an argument is a date-time, a complementary date-time conversion is required. * This method return which separator must be used to separate the symbol from the date-time conversion. * @return date-time separator */ public String getDateSeparator(String symbol) { ArgumentNameSpace ns = this.namespaces.get(symbol); if( ns == null ) return ""; return ns.getDateSeparator(symbol); } /** Return details to use for a specified symbol * @param details Current details informations picked-up from the pattern specification * @param symbol Which symbol to apply the details * @return New details to use for this argument */ public ArgumentDetails getDetails(ArgumentDetails details, String symbol) { ArgumentNameSpace ns = this.namespaces.get(symbol); if( ns instanceof DetailedArgumentNameSpace ) { return ((DetailedArgumentNameSpace)ns).getDetails(details, symbol); } return details; } /** Invoked before using a symbol by a pattern * @param symbol Symbol marked as used */ public void use(String symbol) { ArgumentNameSpace ns = this.namespaces.get(symbol); if( ns != null ) ns.use(symbol); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy