View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.Serializable;
22  import java.text.DateFormat;
23  import java.text.DateFormatSymbols;
24  import java.text.FieldPosition;
25  import java.util.ArrayList;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.GregorianCalendar;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.TimeZone;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.concurrent.ConcurrentMap;
34  
35  import org.apache.commons.lang3.Validate;
36  
37  /**
38   * <p>FastDatePrinter is a fast and thread-safe version of
39   * {@link java.text.SimpleDateFormat}.</p>
40   *
41   * <p>This class can be used as a direct replacement to
42   * {@code SimpleDateFormat} in most formatting situations.
43   * This class is especially useful in multi-threaded server environments.
44   * {@code SimpleDateFormat} is not thread-safe in any JDK version,
45   * nor will it be as Sun have closed the bug/RFE.
46   * </p>
47   *
48   * <p>Only formatting is supported, but all patterns are compatible with
49   * SimpleDateFormat (except time zones and some year patterns - see below).</p>
50   *
51   * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
52   * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
53   * This pattern letter can be used here (on all JDK versions).</p>
54   *
55   * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
56   * ISO8601 full format time zones (eg. {@code +08:00} or {@code -11:00}).
57   * This introduces a minor incompatibility with Java 1.4, but at a gain of
58   * useful functionality.</p>
59   *
60   * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
61   * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
62   * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
63   * 'YYY' will be formatted as '2003', while it was '03' in former Java
64   * versions. FastDatePrinter implements the behavior of Java 7.</p>
65   *
66   * @version $Id: FastDatePrinter.java 1567799 2014-02-12 23:25:58Z sebb $
67   * @since 3.2
68   */
69  public class FastDatePrinter implements DatePrinter, Serializable {
70      // A lot of the speed in this class comes from caching, but some comes
71      // from the special int to StringBuffer conversion.
72      //
73      // The following produces a padded 2 digit number:
74      //   buffer.append((char)(value / 10 + '0'));
75      //   buffer.append((char)(value % 10 + '0'));
76      //
77      // Note that the fastest append to StringBuffer is a single char (used here).
78      // Note that Integer.toString() is not called, the conversion is simply
79      // taking the value and adding (mathematically) the ASCII value for '0'.
80      // So, don't change this code! It works and is very fast.
81  
82      /**
83       * Required for serialization support.
84       *
85       * @see java.io.Serializable
86       */
87      private static final long serialVersionUID = 1L;
88  
89      /**
90       * FULL locale dependent date or time style.
91       */
92      public static final int FULL = DateFormat.FULL;
93      /**
94       * LONG locale dependent date or time style.
95       */
96      public static final int LONG = DateFormat.LONG;
97      /**
98       * MEDIUM locale dependent date or time style.
99       */
100     public static final int MEDIUM = DateFormat.MEDIUM;
101     /**
102      * SHORT locale dependent date or time style.
103      */
104     public static final int SHORT = DateFormat.SHORT;
105 
106     /**
107      * The pattern.
108      */
109     private final String mPattern;
110     /**
111      * The time zone.
112      */
113     private final TimeZone mTimeZone;
114     /**
115      * The locale.
116      */
117     private final Locale mLocale;
118     /**
119      * The parsed rules.
120      */
121     private transient Rule[] mRules;
122     /**
123      * The estimated maximum length.
124      */
125     private transient int mMaxLengthEstimate;
126 
127     // Constructor
128     //-----------------------------------------------------------------------
129     /**
130      * <p>Constructs a new FastDatePrinter.</p>
131      *
132      * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
133      * @param timeZone  non-null time zone to use
134      * @param locale  non-null locale to use
135      * @throws NullPointerException if pattern, timeZone, or locale is null.
136      */
137     protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
138         mPattern = pattern;
139         mTimeZone = timeZone;
140         mLocale = locale;
141 
142         init();
143     }
144 
145     /**
146      * <p>Initializes the instance for first use.</p>
147      */
148     private void init() {
149         final List<Rule> rulesList = parsePattern();
150         mRules = rulesList.toArray(new Rule[rulesList.size()]);
151 
152         int len = 0;
153         for (int i=mRules.length; --i >= 0; ) {
154             len += mRules[i].estimateLength();
155         }
156 
157         mMaxLengthEstimate = len;
158     }
159 
160     // Parse the pattern
161     //-----------------------------------------------------------------------
162     /**
163      * <p>Returns a list of Rules given a pattern.</p>
164      *
165      * @return a {@code List} of Rule objects
166      * @throws IllegalArgumentException if pattern is invalid
167      */
168     protected List<Rule> parsePattern() {
169         final DateFormatSymbols symbols = new DateFormatSymbols(mLocale);
170         final List<Rule> rules = new ArrayList<Rule>();
171 
172         final String[] ERAs = symbols.getEras();
173         final String[] months = symbols.getMonths();
174         final String[] shortMonths = symbols.getShortMonths();
175         final String[] weekdays = symbols.getWeekdays();
176         final String[] shortWeekdays = symbols.getShortWeekdays();
177         final String[] AmPmStrings = symbols.getAmPmStrings();
178 
179         final int length = mPattern.length();
180         final int[] indexRef = new int[1];
181 
182         for (int i = 0; i < length; i++) {
183             indexRef[0] = i;
184             final String token = parseToken(mPattern, indexRef);
185             i = indexRef[0];
186 
187             final int tokenLen = token.length();
188             if (tokenLen == 0) {
189                 break;
190             }
191 
192             Rule rule;
193             final char c = token.charAt(0);
194 
195             switch (c) {
196             case 'G': // era designator (text)
197                 rule = new TextField(Calendar.ERA, ERAs);
198                 break;
199             case 'y': // year (number)
200                 if (tokenLen == 2) {
201                     rule = TwoDigitYearField.INSTANCE;
202                 } else {
203                     rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen);
204                 }
205                 break;
206             case 'M': // month in year (text and number)
207                 if (tokenLen >= 4) {
208                     rule = new TextField(Calendar.MONTH, months);
209                 } else if (tokenLen == 3) {
210                     rule = new TextField(Calendar.MONTH, shortMonths);
211                 } else if (tokenLen == 2) {
212                     rule = TwoDigitMonthField.INSTANCE;
213                 } else {
214                     rule = UnpaddedMonthField.INSTANCE;
215                 }
216                 break;
217             case 'd': // day in month (number)
218                 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
219                 break;
220             case 'h': // hour in am/pm (number, 1..12)
221                 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
222                 break;
223             case 'H': // hour in day (number, 0..23)
224                 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
225                 break;
226             case 'm': // minute in hour (number)
227                 rule = selectNumberRule(Calendar.MINUTE, tokenLen);
228                 break;
229             case 's': // second in minute (number)
230                 rule = selectNumberRule(Calendar.SECOND, tokenLen);
231                 break;
232             case 'S': // millisecond (number)
233                 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
234                 break;
235             case 'E': // day in week (text)
236                 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
237                 break;
238             case 'D': // day in year (number)
239                 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
240                 break;
241             case 'F': // day of week in month (number)
242                 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
243                 break;
244             case 'w': // week in year (number)
245                 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
246                 break;
247             case 'W': // week in month (number)
248                 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
249                 break;
250             case 'a': // am/pm marker (text)
251                 rule = new TextField(Calendar.AM_PM, AmPmStrings);
252                 break;
253             case 'k': // hour in day (1..24)
254                 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
255                 break;
256             case 'K': // hour in am/pm (0..11)
257                 rule = selectNumberRule(Calendar.HOUR, tokenLen);
258                 break;
259             case 'z': // time zone (text)
260                 if (tokenLen >= 4) {
261                     rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.LONG);
262                 } else {
263                     rule = new TimeZoneNameRule(mTimeZone, mLocale, TimeZone.SHORT);
264                 }
265                 break;
266             case 'Z': // time zone (value)
267                 if (tokenLen == 1) {
268                     rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
269                 } else {
270                     rule = TimeZoneNumberRule.INSTANCE_COLON;
271                 }
272                 break;
273             case '\'': // literal text
274                 final String sub = token.substring(1);
275                 if (sub.length() == 1) {
276                     rule = new CharacterLiteral(sub.charAt(0));
277                 } else {
278                     rule = new StringLiteral(sub);
279                 }
280                 break;
281             default:
282                 throw new IllegalArgumentException("Illegal pattern component: " + token);
283             }
284 
285             rules.add(rule);
286         }
287 
288         return rules;
289     }
290 
291     /**
292      * <p>Performs the parsing of tokens.</p>
293      *
294      * @param pattern  the pattern
295      * @param indexRef  index references
296      * @return parsed token
297      */
298     protected String parseToken(final String pattern, final int[] indexRef) {
299         final StringBuilder buf = new StringBuilder();
300 
301         int i = indexRef[0];
302         final int length = pattern.length();
303 
304         char c = pattern.charAt(i);
305         if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
306             // Scan a run of the same character, which indicates a time
307             // pattern.
308             buf.append(c);
309 
310             while (i + 1 < length) {
311                 final char peek = pattern.charAt(i + 1);
312                 if (peek == c) {
313                     buf.append(c);
314                     i++;
315                 } else {
316                     break;
317                 }
318             }
319         } else {
320             // This will identify token as text.
321             buf.append('\'');
322 
323             boolean inLiteral = false;
324 
325             for (; i < length; i++) {
326                 c = pattern.charAt(i);
327 
328                 if (c == '\'') {
329                     if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
330                         // '' is treated as escaped '
331                         i++;
332                         buf.append(c);
333                     } else {
334                         inLiteral = !inLiteral;
335                     }
336                 } else if (!inLiteral &&
337                          (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
338                     i--;
339                     break;
340                 } else {
341                     buf.append(c);
342                 }
343             }
344         }
345 
346         indexRef[0] = i;
347         return buf.toString();
348     }
349 
350     /**
351      * <p>Gets an appropriate rule for the padding required.</p>
352      *
353      * @param field  the field to get a rule for
354      * @param padding  the padding required
355      * @return a new rule with the correct padding
356      */
357     protected NumberRule selectNumberRule(final int field, final int padding) {
358         switch (padding) {
359         case 1:
360             return new UnpaddedNumberField(field);
361         case 2:
362             return new TwoDigitNumberField(field);
363         default:
364             return new PaddedNumberField(field, padding);
365         }
366     }
367 
368     // Format methods
369     //-----------------------------------------------------------------------
370     /**
371      * <p>Formats a {@code Date}, {@code Calendar} or
372      * {@code Long} (milliseconds) object.</p>
373      *
374      * @param obj  the object to format
375      * @param toAppendTo  the buffer to append to
376      * @param pos  the position - ignored
377      * @return the buffer passed in
378      */
379     @Override
380     public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
381         if (obj instanceof Date) {
382             return format((Date) obj, toAppendTo);
383         } else if (obj instanceof Calendar) {
384             return format((Calendar) obj, toAppendTo);
385         } else if (obj instanceof Long) {
386             return format(((Long) obj).longValue(), toAppendTo);
387         } else {
388             throw new IllegalArgumentException("Unknown class: " +
389                 (obj == null ? "<null>" : obj.getClass().getName()));
390         }
391     }
392 
393     /* (non-Javadoc)
394      * @see org.apache.commons.lang3.time.DatePrinter#format(long)
395      */
396     @Override
397     public String format(final long millis) {
398         final Calendar c = newCalendar();  // hard code GregorianCalendar
399         c.setTimeInMillis(millis);
400         return applyRulesToString(c);
401     }
402 
403     /**
404      * Creates a String representation of the given Calendar by applying the rules of this printer to it.
405      * @param c the Calender to apply the rules to.
406      * @return a String representation of the given Calendar.
407      */
408     private String applyRulesToString(final Calendar c) {
409         return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString();
410     }
411 
412     /**
413      * Creation method for ne calender instances.
414      * @return a new Calendar instance.
415      */
416     private GregorianCalendar newCalendar() {
417         // hard code GregorianCalendar
418         return new GregorianCalendar(mTimeZone, mLocale);
419     }
420 
421     /* (non-Javadoc)
422      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
423      */
424     @Override
425     public String format(final Date date) {
426         final Calendar c = newCalendar();  // hard code GregorianCalendar
427         c.setTime(date);
428         return applyRulesToString(c);
429     }
430 
431     /* (non-Javadoc)
432      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
433      */
434     @Override
435     public String format(final Calendar calendar) {
436         return format(calendar, new StringBuffer(mMaxLengthEstimate)).toString();
437     }
438 
439     /* (non-Javadoc)
440      * @see org.apache.commons.lang3.time.DatePrinter#format(long, java.lang.StringBuffer)
441      */
442     @Override
443     public StringBuffer format(final long millis, final StringBuffer buf) {
444         return format(new Date(millis), buf);
445     }
446 
447     /* (non-Javadoc)
448      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, java.lang.StringBuffer)
449      */
450     @Override
451     public StringBuffer format(final Date date, final StringBuffer buf) {
452         final Calendar c = newCalendar();  // hard code GregorianCalendar
453         c.setTime(date);
454         return applyRules(c, buf);
455     }
456 
457     /* (non-Javadoc)
458      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.StringBuffer)
459      */
460     @Override
461     public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
462         return applyRules(calendar, buf);
463     }
464 
465     /**
466      * <p>Performs the formatting by applying the rules to the
467      * specified calendar.</p>
468      *
469      * @param calendar  the calendar to format
470      * @param buf  the buffer to format into
471      * @return the specified string buffer
472      */
473     protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) {
474         for (final Rule rule : mRules) {
475             rule.appendTo(buf, calendar);
476         }
477         return buf;
478     }
479 
480     // Accessors
481     //-----------------------------------------------------------------------
482     /* (non-Javadoc)
483      * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
484      */
485     @Override
486     public String getPattern() {
487         return mPattern;
488     }
489 
490     /* (non-Javadoc)
491      * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
492      */
493     @Override
494     public TimeZone getTimeZone() {
495         return mTimeZone;
496     }
497 
498     /* (non-Javadoc)
499      * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
500      */
501     @Override
502     public Locale getLocale() {
503         return mLocale;
504     }
505 
506     /**
507      * <p>Gets an estimate for the maximum string length that the
508      * formatter will produce.</p>
509      *
510      * <p>The actual formatted length will almost always be less than or
511      * equal to this amount.</p>
512      *
513      * @return the maximum formatted length
514      */
515     public int getMaxLengthEstimate() {
516         return mMaxLengthEstimate;
517     }
518 
519     // Basics
520     //-----------------------------------------------------------------------
521     /**
522      * <p>Compares two objects for equality.</p>
523      *
524      * @param obj  the object to compare to
525      * @return {@code true} if equal
526      */
527     @Override
528     public boolean equals(final Object obj) {
529         if (obj instanceof FastDatePrinter == false) {
530             return false;
531         }
532         final FastDatePrinter other = (FastDatePrinter) obj;
533         return mPattern.equals(other.mPattern)
534             && mTimeZone.equals(other.mTimeZone) 
535             && mLocale.equals(other.mLocale);
536     }
537 
538     /**
539      * <p>Returns a hashcode compatible with equals.</p>
540      *
541      * @return a hashcode compatible with equals
542      */
543     @Override
544     public int hashCode() {
545         return mPattern.hashCode() + 13 * (mTimeZone.hashCode() + 13 * mLocale.hashCode());
546     }
547 
548     /**
549      * <p>Gets a debugging string version of this formatter.</p>
550      *
551      * @return a debugging string
552      */
553     @Override
554     public String toString() {
555         return "FastDatePrinter[" + mPattern + "," + mLocale + "," + mTimeZone.getID() + "]";
556     }
557 
558     // Serializing
559     //-----------------------------------------------------------------------
560     /**
561      * Create the object after serialization. This implementation reinitializes the
562      * transient properties.
563      *
564      * @param in ObjectInputStream from which the object is being deserialized.
565      * @throws IOException if there is an IO issue.
566      * @throws ClassNotFoundException if a class cannot be found.
567      */
568     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
569         in.defaultReadObject();
570         init();
571     }
572 
573     // Rules
574     //-----------------------------------------------------------------------
575     /**
576      * <p>Inner class defining a rule.</p>
577      */
578     private interface Rule {
579         /**
580          * Returns the estimated lentgh of the result.
581          *
582          * @return the estimated length
583          */
584         int estimateLength();
585 
586         /**
587          * Appends the value of the specified calendar to the output buffer based on the rule implementation.
588          *
589          * @param buffer the output buffer
590          * @param calendar calendar to be appended
591          */
592         void appendTo(StringBuffer buffer, Calendar calendar);
593     }
594 
595     /**
596      * <p>Inner class defining a numeric rule.</p>
597      */
598     private interface NumberRule extends Rule {
599         /**
600          * Appends the specified value to the output buffer based on the rule implementation.
601          *
602          * @param buffer the output buffer
603          * @param value the value to be appended
604          */
605         void appendTo(StringBuffer buffer, int value);
606     }
607 
608     /**
609      * <p>Inner class to output a constant single character.</p>
610      */
611     private static class CharacterLiteral implements Rule {
612         private final char mValue;
613 
614         /**
615          * Constructs a new instance of {@code CharacterLiteral}
616          * to hold the specified value.
617          *
618          * @param value the character literal
619          */
620         CharacterLiteral(final char value) {
621             mValue = value;
622         }
623 
624         /**
625          * {@inheritDoc}
626          */
627         @Override
628         public int estimateLength() {
629             return 1;
630         }
631 
632         /**
633          * {@inheritDoc}
634          */
635         @Override
636         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
637             buffer.append(mValue);
638         }
639     }
640 
641     /**
642      * <p>Inner class to output a constant string.</p>
643      */
644     private static class StringLiteral implements Rule {
645         private final String mValue;
646 
647         /**
648          * Constructs a new instance of {@code StringLiteral}
649          * to hold the specified value.
650          *
651          * @param value the string literal
652          */
653         StringLiteral(final String value) {
654             mValue = value;
655         }
656 
657         /**
658          * {@inheritDoc}
659          */
660         @Override
661         public int estimateLength() {
662             return mValue.length();
663         }
664 
665         /**
666          * {@inheritDoc}
667          */
668         @Override
669         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
670             buffer.append(mValue);
671         }
672     }
673 
674     /**
675      * <p>Inner class to output one of a set of values.</p>
676      */
677     private static class TextField implements Rule {
678         private final int mField;
679         private final String[] mValues;
680 
681         /**
682          * Constructs an instance of {@code TextField}
683          * with the specified field and values.
684          *
685          * @param field the field
686          * @param values the field values
687          */
688         TextField(final int field, final String[] values) {
689             mField = field;
690             mValues = values;
691         }
692 
693         /**
694          * {@inheritDoc}
695          */
696         @Override
697         public int estimateLength() {
698             int max = 0;
699             for (int i=mValues.length; --i >= 0; ) {
700                 final int len = mValues[i].length();
701                 if (len > max) {
702                     max = len;
703                 }
704             }
705             return max;
706         }
707 
708         /**
709          * {@inheritDoc}
710          */
711         @Override
712         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
713             buffer.append(mValues[calendar.get(mField)]);
714         }
715     }
716 
717     /**
718      * <p>Inner class to output an unpadded number.</p>
719      */
720     private static class UnpaddedNumberField implements NumberRule {
721         private final int mField;
722 
723         /**
724          * Constructs an instance of {@code UnpadedNumberField} with the specified field.
725          *
726          * @param field the field
727          */
728         UnpaddedNumberField(final int field) {
729             mField = field;
730         }
731 
732         /**
733          * {@inheritDoc}
734          */
735         @Override
736         public int estimateLength() {
737             return 4;
738         }
739 
740         /**
741          * {@inheritDoc}
742          */
743         @Override
744         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
745             appendTo(buffer, calendar.get(mField));
746         }
747 
748         /**
749          * {@inheritDoc}
750          */
751         @Override
752         public final void appendTo(final StringBuffer buffer, final int value) {
753             if (value < 10) {
754                 buffer.append((char)(value + '0'));
755             } else if (value < 100) {
756                 buffer.append((char)(value / 10 + '0'));
757                 buffer.append((char)(value % 10 + '0'));
758             } else {
759                 buffer.append(Integer.toString(value));
760             }
761         }
762     }
763 
764     /**
765      * <p>Inner class to output an unpadded month.</p>
766      */
767     private static class UnpaddedMonthField implements NumberRule {
768         static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();
769 
770         /**
771          * Constructs an instance of {@code UnpaddedMonthField}.
772          *
773          */
774         UnpaddedMonthField() {
775             super();
776         }
777 
778         /**
779          * {@inheritDoc}
780          */
781         @Override
782         public int estimateLength() {
783             return 2;
784         }
785 
786         /**
787          * {@inheritDoc}
788          */
789         @Override
790         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
791             appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
792         }
793 
794         /**
795          * {@inheritDoc}
796          */
797         @Override
798         public final void appendTo(final StringBuffer buffer, final int value) {
799             if (value < 10) {
800                 buffer.append((char)(value + '0'));
801             } else {
802                 buffer.append((char)(value / 10 + '0'));
803                 buffer.append((char)(value % 10 + '0'));
804             }
805         }
806     }
807 
808     /**
809      * <p>Inner class to output a padded number.</p>
810      */
811     private static class PaddedNumberField implements NumberRule {
812         private final int mField;
813         private final int mSize;
814 
815         /**
816          * Constructs an instance of {@code PaddedNumberField}.
817          *
818          * @param field the field
819          * @param size size of the output field
820          */
821         PaddedNumberField(final int field, final int size) {
822             if (size < 3) {
823                 // Should use UnpaddedNumberField or TwoDigitNumberField.
824                 throw new IllegalArgumentException();
825             }
826             mField = field;
827             mSize = size;
828         }
829 
830         /**
831          * {@inheritDoc}
832          */
833         @Override
834         public int estimateLength() {
835             return 4;
836         }
837 
838         /**
839          * {@inheritDoc}
840          */
841         @Override
842         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
843             appendTo(buffer, calendar.get(mField));
844         }
845 
846         /**
847          * {@inheritDoc}
848          */
849         @Override
850         public final void appendTo(final StringBuffer buffer, final int value) {
851             if (value < 100) {
852                 for (int i = mSize; --i >= 2; ) {
853                     buffer.append('0');
854                 }
855                 buffer.append((char)(value / 10 + '0'));
856                 buffer.append((char)(value % 10 + '0'));
857             } else {
858                 int digits;
859                 if (value < 1000) {
860                     digits = 3;
861                 } else {
862                     Validate.isTrue(value > -1, "Negative values should not be possible", value);
863                     digits = Integer.toString(value).length();
864                 }
865                 for (int i = mSize; --i >= digits; ) {
866                     buffer.append('0');
867                 }
868                 buffer.append(Integer.toString(value));
869             }
870         }
871     }
872 
873     /**
874      * <p>Inner class to output a two digit number.</p>
875      */
876     private static class TwoDigitNumberField implements NumberRule {
877         private final int mField;
878 
879         /**
880          * Constructs an instance of {@code TwoDigitNumberField} with the specified field.
881          *
882          * @param field the field
883          */
884         TwoDigitNumberField(final int field) {
885             mField = field;
886         }
887 
888         /**
889          * {@inheritDoc}
890          */
891         @Override
892         public int estimateLength() {
893             return 2;
894         }
895 
896         /**
897          * {@inheritDoc}
898          */
899         @Override
900         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
901             appendTo(buffer, calendar.get(mField));
902         }
903 
904         /**
905          * {@inheritDoc}
906          */
907         @Override
908         public final void appendTo(final StringBuffer buffer, final int value) {
909             if (value < 100) {
910                 buffer.append((char)(value / 10 + '0'));
911                 buffer.append((char)(value % 10 + '0'));
912             } else {
913                 buffer.append(Integer.toString(value));
914             }
915         }
916     }
917 
918     /**
919      * <p>Inner class to output a two digit year.</p>
920      */
921     private static class TwoDigitYearField implements NumberRule {
922         static final TwoDigitYearField INSTANCE = new TwoDigitYearField();
923 
924         /**
925          * Constructs an instance of {@code TwoDigitYearField}.
926          */
927         TwoDigitYearField() {
928             super();
929         }
930 
931         /**
932          * {@inheritDoc}
933          */
934         @Override
935         public int estimateLength() {
936             return 2;
937         }
938 
939         /**
940          * {@inheritDoc}
941          */
942         @Override
943         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
944             appendTo(buffer, calendar.get(Calendar.YEAR) % 100);
945         }
946 
947         /**
948          * {@inheritDoc}
949          */
950         @Override
951         public final void appendTo(final StringBuffer buffer, final int value) {
952             buffer.append((char)(value / 10 + '0'));
953             buffer.append((char)(value % 10 + '0'));
954         }
955     }
956 
957     /**
958      * <p>Inner class to output a two digit month.</p>
959      */
960     private static class TwoDigitMonthField implements NumberRule {
961         static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();
962 
963         /**
964          * Constructs an instance of {@code TwoDigitMonthField}.
965          */
966         TwoDigitMonthField() {
967             super();
968         }
969 
970         /**
971          * {@inheritDoc}
972          */
973         @Override
974         public int estimateLength() {
975             return 2;
976         }
977 
978         /**
979          * {@inheritDoc}
980          */
981         @Override
982         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
983             appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
984         }
985 
986         /**
987          * {@inheritDoc}
988          */
989         @Override
990         public final void appendTo(final StringBuffer buffer, final int value) {
991             buffer.append((char)(value / 10 + '0'));
992             buffer.append((char)(value % 10 + '0'));
993         }
994     }
995 
996     /**
997      * <p>Inner class to output the twelve hour field.</p>
998      */
999     private static class TwelveHourField implements NumberRule {
1000         private final NumberRule mRule;
1001 
1002         /**
1003          * Constructs an instance of {@code TwelveHourField} with the specified
1004          * {@code NumberRule}.
1005          *
1006          * @param rule the rule
1007          */
1008         TwelveHourField(final NumberRule rule) {
1009             mRule = rule;
1010         }
1011 
1012         /**
1013          * {@inheritDoc}
1014          */
1015         @Override
1016         public int estimateLength() {
1017             return mRule.estimateLength();
1018         }
1019 
1020         /**
1021          * {@inheritDoc}
1022          */
1023         @Override
1024         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1025             int value = calendar.get(Calendar.HOUR);
1026             if (value == 0) {
1027                 value = calendar.getLeastMaximum(Calendar.HOUR) + 1;
1028             }
1029             mRule.appendTo(buffer, value);
1030         }
1031 
1032         /**
1033          * {@inheritDoc}
1034          */
1035         @Override
1036         public void appendTo(final StringBuffer buffer, final int value) {
1037             mRule.appendTo(buffer, value);
1038         }
1039     }
1040 
1041     /**
1042      * <p>Inner class to output the twenty four hour field.</p>
1043      */
1044     private static class TwentyFourHourField implements NumberRule {
1045         private final NumberRule mRule;
1046 
1047         /**
1048          * Constructs an instance of {@code TwentyFourHourField} with the specified
1049          * {@code NumberRule}.
1050          *
1051          * @param rule the rule
1052          */
1053         TwentyFourHourField(final NumberRule rule) {
1054             mRule = rule;
1055         }
1056 
1057         /**
1058          * {@inheritDoc}
1059          */
1060         @Override
1061         public int estimateLength() {
1062             return mRule.estimateLength();
1063         }
1064 
1065         /**
1066          * {@inheritDoc}
1067          */
1068         @Override
1069         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1070             int value = calendar.get(Calendar.HOUR_OF_DAY);
1071             if (value == 0) {
1072                 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;
1073             }
1074             mRule.appendTo(buffer, value);
1075         }
1076 
1077         /**
1078          * {@inheritDoc}
1079          */
1080         @Override
1081         public void appendTo(final StringBuffer buffer, final int value) {
1082             mRule.appendTo(buffer, value);
1083         }
1084     }
1085 
1086     //-----------------------------------------------------------------------
1087 
1088     private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache =
1089         new ConcurrentHashMap<TimeZoneDisplayKey, String>(7);
1090     /**
1091      * <p>Gets the time zone display name, using a cache for performance.</p>
1092      *
1093      * @param tz  the zone to query
1094      * @param daylight  true if daylight savings
1095      * @param style  the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
1096      * @param locale  the locale to use
1097      * @return the textual name of the time zone
1098      */
1099     static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
1100         final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
1101         String value = cTimeZoneDisplayCache.get(key);
1102         if (value == null) {
1103             // This is a very slow call, so cache the results.
1104             value = tz.getDisplayName(daylight, style, locale);
1105             final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value);
1106             if (prior != null) {
1107                 value= prior;
1108             }
1109         }
1110         return value;
1111     }
1112 
1113     /**
1114      * <p>Inner class to output a time zone name.</p>
1115      */
1116     private static class TimeZoneNameRule implements Rule {
1117         private final Locale mLocale;
1118         private final int mStyle;
1119         private final String mStandard;
1120         private final String mDaylight;
1121 
1122         /**
1123          * Constructs an instance of {@code TimeZoneNameRule} with the specified properties.
1124          *
1125          * @param timeZone the time zone
1126          * @param locale the locale
1127          * @param style the style
1128          */
1129         TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) {
1130             mLocale = locale;
1131             mStyle = style;
1132             
1133             mStandard = getTimeZoneDisplay(timeZone, false, style, locale);
1134             mDaylight = getTimeZoneDisplay(timeZone, true, style, locale);
1135         }
1136 
1137         /**
1138          * {@inheritDoc}
1139          */
1140         @Override
1141         public int estimateLength() {
1142             // We have no access to the Calendar object that will be passed to
1143             // appendTo so base estimate on the TimeZone passed to the
1144             // constructor
1145             return Math.max(mStandard.length(), mDaylight.length());
1146         }
1147 
1148         /**
1149          * {@inheritDoc}
1150          */
1151         @Override
1152         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1153             final TimeZone zone = calendar.getTimeZone();
1154             if (zone.useDaylightTime()
1155                     && calendar.get(Calendar.DST_OFFSET) != 0) {
1156                 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
1157             } else {
1158                 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));
1159             }
1160         }
1161     }
1162 
1163     /**
1164      * <p>Inner class to output a time zone as a number {@code +/-HHMM}
1165      * or {@code +/-HH:MM}.</p>
1166      */
1167     private static class TimeZoneNumberRule implements Rule {
1168         static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);
1169         static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);
1170 
1171         final boolean mColon;
1172 
1173         /**
1174          * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties.
1175          *
1176          * @param colon add colon between HH and MM in the output if {@code true}
1177          */
1178         TimeZoneNumberRule(final boolean colon) {
1179             mColon = colon;
1180         }
1181 
1182         /**
1183          * {@inheritDoc}
1184          */
1185         @Override
1186         public int estimateLength() {
1187             return 5;
1188         }
1189 
1190         /**
1191          * {@inheritDoc}
1192          */
1193         @Override
1194         public void appendTo(final StringBuffer buffer, final Calendar calendar) {
1195             int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
1196 
1197             if (offset < 0) {
1198                 buffer.append('-');
1199                 offset = -offset;
1200             } else {
1201                 buffer.append('+');
1202             }
1203 
1204             final int hours = offset / (60 * 60 * 1000);
1205             buffer.append((char)(hours / 10 + '0'));
1206             buffer.append((char)(hours % 10 + '0'));
1207 
1208             if (mColon) {
1209                 buffer.append(':');
1210             }
1211 
1212             final int minutes = offset / (60 * 1000) - 60 * hours;
1213             buffer.append((char)(minutes / 10 + '0'));
1214             buffer.append((char)(minutes % 10 + '0'));
1215         }
1216     }
1217 
1218     // ----------------------------------------------------------------------
1219     /**
1220      * <p>Inner class that acts as a compound key for time zone names.</p>
1221      */
1222     private static class TimeZoneDisplayKey {
1223         private final TimeZone mTimeZone;
1224         private final int mStyle;
1225         private final Locale mLocale;
1226 
1227         /**
1228          * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties.
1229          *
1230          * @param timeZone the time zone
1231          * @param daylight adjust the style for daylight saving time if {@code true}
1232          * @param style the timezone style
1233          * @param locale the timezone locale
1234          */
1235         TimeZoneDisplayKey(final TimeZone timeZone,
1236                            final boolean daylight, final int style, final Locale locale) {
1237             mTimeZone = timeZone;
1238             if (daylight) {
1239                 mStyle = style | 0x80000000;
1240             } else {
1241                 mStyle = style;
1242             }
1243             mLocale = locale;
1244         }
1245 
1246         /**
1247          * {@inheritDoc}
1248          */
1249         @Override
1250         public int hashCode() {
1251             return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode();
1252         }
1253 
1254         /**
1255          * {@inheritDoc}
1256          */
1257         @Override
1258         public boolean equals(final Object obj) {
1259             if (this == obj) {
1260                 return true;
1261             }
1262             if (obj instanceof TimeZoneDisplayKey) {
1263                 final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj;
1264                 return
1265                     mTimeZone.equals(other.mTimeZone) &&
1266                     mStyle == other.mStyle &&
1267                     mLocale.equals(other.mLocale);
1268             }
1269             return false;
1270         }
1271     }
1272 }