001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.time;
018
019import java.io.IOException;
020import java.io.ObjectInputStream;
021import java.io.Serializable;
022import java.text.DateFormat;
023import java.text.DateFormatSymbols;
024import java.text.FieldPosition;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.GregorianCalendar;
029import java.util.List;
030import java.util.Locale;
031import java.util.TimeZone;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.ConcurrentMap;
034
035import org.apache.commons.lang3.Validate;
036
037/**
038 * <p>FastDatePrinter is a fast and thread-safe version of
039 * {@link java.text.SimpleDateFormat}.</p>
040 *
041 * <p>This class can be used as a direct replacement to
042 * {@code SimpleDateFormat} in most formatting situations.
043 * This class is especially useful in multi-threaded server environments.
044 * {@code SimpleDateFormat} is not thread-safe in any JDK version,
045 * nor will it be as Sun have closed the bug/RFE.
046 * </p>
047 *
048 * <p>Only formatting is supported, but all patterns are compatible with
049 * SimpleDateFormat (except time zones and some year patterns - see below).</p>
050 *
051 * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
052 * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
053 * This pattern letter can be used here (on all JDK versions).</p>
054 *
055 * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
056 * ISO8601 full format time zones (eg. {@code +08:00} or {@code -11:00}).
057 * This introduces a minor incompatibility with Java 1.4, but at a gain of
058 * useful functionality.</p>
059 *
060 * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
061 * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
062 * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
063 * 'YYY' will be formatted as '2003', while it was '03' in former Java
064 * versions. FastDatePrinter implements the behavior of Java 7.</p>
065 *
066 * @version $Id: FastDatePrinter.java 1552661 2013-12-20 13:36:30Z britter $
067 * @since 3.2
068 */
069public class FastDatePrinter implements DatePrinter, Serializable {
070    // A lot of the speed in this class comes from caching, but some comes
071    // from the special int to StringBuffer conversion.
072    //
073    // The following produces a padded 2 digit number:
074    //   buffer.append((char)(value / 10 + '0'));
075    //   buffer.append((char)(value % 10 + '0'));
076    //
077    // Note that the fastest append to StringBuffer is a single char (used here).
078    // Note that Integer.toString() is not called, the conversion is simply
079    // taking the value and adding (mathematically) the ASCII value for '0'.
080    // So, don't change this code! It works and is very fast.
081
082    /**
083     * Required for serialization support.
084     *
085     * @see java.io.Serializable
086     */
087    private static final long serialVersionUID = 1L;
088
089    /**
090     * FULL locale dependent date or time style.
091     */
092    public static final int FULL = DateFormat.FULL;
093    /**
094     * LONG locale dependent date or time style.
095     */
096    public static final int LONG = DateFormat.LONG;
097    /**
098     * MEDIUM locale dependent date or time style.
099     */
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 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, int style, final Locale locale) {
1237            mTimeZone = timeZone;
1238            if (daylight) {
1239                style |= 0x80000000;
1240            }
1241            mStyle = style;
1242            mLocale = locale;
1243        }
1244
1245        /**
1246         * {@inheritDoc}
1247         */
1248        @Override
1249        public int hashCode() {
1250            return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode();
1251        }
1252
1253        /**
1254         * {@inheritDoc}
1255         */
1256        @Override
1257        public boolean equals(final Object obj) {
1258            if (this == obj) {
1259                return true;
1260            }
1261            if (obj instanceof TimeZoneDisplayKey) {
1262                final TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj;
1263                return
1264                    mTimeZone.equals(other.mTimeZone) &&
1265                    mStyle == other.mStyle &&
1266                    mLocale.equals(other.mLocale);
1267            }
1268            return false;
1269        }
1270    }
1271}