• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.text.SimpleDateFormat;
26 import java.util.ArrayList;
27 import java.util.Calendar;
28 import java.util.Date;
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.ClassUtils;
36 import org.apache.commons.lang3.LocaleUtils;
37 import org.apache.commons.lang3.exception.ExceptionUtils;
38 
39 /**
40  * FastDatePrinter is a fast and thread-safe version of
41  * {@link java.text.SimpleDateFormat}.
42  *
43  * <p>To obtain a FastDatePrinter, use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}
44  * or another variation of the factory methods of {@link FastDateFormat}.</p>
45  *
46  * <p>Since FastDatePrinter is thread safe, you can use a static member instance:</p>
47  * <code>
48  *     private static final DatePrinter DATE_PRINTER = FastDateFormat.getInstance("yyyy-MM-dd");
49  * </code>
50  *
51  * <p>This class can be used as a direct replacement to
52  * {@link SimpleDateFormat} in most formatting situations.
53  * This class is especially useful in multi-threaded server environments.
54  * {@link SimpleDateFormat} is not thread-safe in any JDK version,
55  * nor will it be as Sun have closed the bug/RFE.
56  * </p>
57  *
58  * <p>Only formatting is supported by this class, but all patterns are compatible with
59  * SimpleDateFormat (except time zones and some year patterns - see below).</p>
60  *
61  * <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
62  * time zones in RFC822 format (eg. {@code +0800} or {@code -1100}).
63  * This pattern letter can be used here (on all JDK versions).</p>
64  *
65  * <p>In addition, the pattern {@code 'ZZ'} has been made to represent
66  * ISO 8601 extended format time zones (eg. {@code +08:00} or {@code -11:00}).
67  * This introduces a minor incompatibility with Java 1.4, but at a gain of
68  * useful functionality.</p>
69  *
70  * <p>Starting with JDK7, ISO 8601 support was added using the pattern {@code 'X'}.
71  * To maintain compatibility, {@code 'ZZ'} will continue to be supported, but using
72  * one of the {@code 'X'} formats is recommended.
73  *
74  * <p>Javadoc cites for the year pattern: <i>For formatting, if the number of
75  * pattern letters is 2, the year is truncated to 2 digits; otherwise it is
76  * interpreted as a number.</i> Starting with Java 1.7 a pattern of 'Y' or
77  * 'YYY' will be formatted as '2003', while it was '03' in former Java
78  * versions. FastDatePrinter implements the behavior of Java 7.</p>
79  *
80  * @since 3.2
81  * @see FastDateParser
82  */
83 public class FastDatePrinter implements DatePrinter, Serializable {
84     // A lot of the speed in this class comes from caching, but some comes
85     // from the special int to StringBuffer conversion.
86     //
87     // The following produces a padded 2-digit number:
88     //   buffer.append((char)(value / 10 + '0'));
89     //   buffer.append((char)(value % 10 + '0'));
90     //
91     // Note that the fastest append to StringBuffer is a single char (used here).
92     // Note that Integer.toString() is not called, the conversion is simply
93     // taking the value and adding (mathematically) the ASCII value for '0'.
94     // So, don't change this code! It works and is very fast.
95 
96     /** Empty array. */
97     private static final Rule[] EMPTY_RULE_ARRAY = {};
98 
99     /**
100      * Required for serialization support.
101      *
102      * @see java.io.Serializable
103      */
104     private static final long serialVersionUID = 1L;
105 
106     /**
107      * FULL locale dependent date or time style.
108      */
109     public static final int FULL = DateFormat.FULL;
110     /**
111      * LONG locale dependent date or time style.
112      */
113     public static final int LONG = DateFormat.LONG;
114     /**
115      * MEDIUM locale dependent date or time style.
116      */
117     public static final int MEDIUM = DateFormat.MEDIUM;
118     /**
119      * SHORT locale dependent date or time style.
120      */
121     public static final int SHORT = DateFormat.SHORT;
122 
123     /**
124      * The pattern.
125      */
126     private final String pattern;
127     /**
128      * The time zone.
129      */
130     private final TimeZone timeZone;
131     /**
132      * The locale.
133      */
134     private final Locale locale;
135     /**
136      * The parsed rules.
137      */
138     private transient Rule[] rules;
139     /**
140      * The estimated maximum length.
141      */
142     private transient int maxLengthEstimate;
143 
144     // Constructor
145     /**
146      * Constructs a new FastDatePrinter.
147      * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)}  or another variation of the
148      * factory methods of {@link FastDateFormat} to get a cached FastDatePrinter instance.
149      *
150      * @param pattern  {@link java.text.SimpleDateFormat} compatible pattern
151      * @param timeZone  non-null time zone to use
152      * @param locale  non-null locale to use
153      * @throws NullPointerException if pattern, timeZone, or locale is null.
154      */
FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale)155     protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
156         this.pattern = pattern;
157         this.timeZone = timeZone;
158         this.locale = LocaleUtils.toLocale(locale);
159         init();
160     }
161 
162     /**
163      * Initializes the instance for first use.
164      */
init()165     private void init() {
166         final List<Rule> rulesList = parsePattern();
167         rules = rulesList.toArray(EMPTY_RULE_ARRAY);
168 
169         int len = 0;
170         for (int i = rules.length; --i >= 0;) {
171             len += rules[i].estimateLength();
172         }
173 
174         maxLengthEstimate = len;
175     }
176 
177     // Parse the pattern
178     /**
179      * Returns a list of Rules given a pattern.
180      *
181      * @return a {@link List} of Rule objects
182      * @throws IllegalArgumentException if pattern is invalid
183      */
parsePattern()184     protected List<Rule> parsePattern() {
185         final DateFormatSymbols symbols = new DateFormatSymbols(locale);
186         final List<Rule> rules = new ArrayList<>();
187 
188         final String[] ERAs = symbols.getEras();
189         final String[] months = symbols.getMonths();
190         final String[] shortMonths = symbols.getShortMonths();
191         final String[] weekdays = symbols.getWeekdays();
192         final String[] shortWeekdays = symbols.getShortWeekdays();
193         final String[] AmPmStrings = symbols.getAmPmStrings();
194 
195         final int length = pattern.length();
196         final int[] indexRef = new int[1];
197 
198         for (int i = 0; i < length; i++) {
199             indexRef[0] = i;
200             final String token = parseToken(pattern, indexRef);
201             i = indexRef[0];
202 
203             final int tokenLen = token.length();
204             if (tokenLen == 0) {
205                 break;
206             }
207 
208             Rule rule;
209             final char c = token.charAt(0);
210 
211             switch (c) {
212             case 'G': // era designator (text)
213                 rule = new TextField(Calendar.ERA, ERAs);
214                 break;
215             case 'y': // year (number)
216             case 'Y': // week year
217                 if (tokenLen == 2) {
218                     rule = TwoDigitYearField.INSTANCE;
219                 } else {
220                     rule = selectNumberRule(Calendar.YEAR, Math.max(tokenLen, 4));
221                 }
222                 if (c == 'Y') {
223                     rule = new WeekYear((NumberRule) rule);
224                 }
225                 break;
226             case 'M': // month in year (text and number)
227                 if (tokenLen >= 4) {
228                     rule = new TextField(Calendar.MONTH, months);
229                 } else if (tokenLen == 3) {
230                     rule = new TextField(Calendar.MONTH, shortMonths);
231                 } else if (tokenLen == 2) {
232                     rule = TwoDigitMonthField.INSTANCE;
233                 } else {
234                     rule = UnpaddedMonthField.INSTANCE;
235                 }
236                 break;
237             case 'L': // month in year (text and number)
238                 if (tokenLen >= 4) {
239                     rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneLongMonthNames());
240                 } else if (tokenLen == 3) {
241                     rule = new TextField(Calendar.MONTH, CalendarUtils.getInstance(locale).getStandaloneShortMonthNames());
242                 } else if (tokenLen == 2) {
243                     rule = TwoDigitMonthField.INSTANCE;
244                 } else {
245                     rule = UnpaddedMonthField.INSTANCE;
246                 }
247                 break;
248             case 'd': // day in month (number)
249                 rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen);
250                 break;
251             case 'h': // hour in am/pm (number, 1..12)
252                 rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen));
253                 break;
254             case 'H': // hour in day (number, 0..23)
255                 rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen);
256                 break;
257             case 'm': // minute in hour (number)
258                 rule = selectNumberRule(Calendar.MINUTE, tokenLen);
259                 break;
260             case 's': // second in minute (number)
261                 rule = selectNumberRule(Calendar.SECOND, tokenLen);
262                 break;
263             case 'S': // millisecond (number)
264                 rule = selectNumberRule(Calendar.MILLISECOND, tokenLen);
265                 break;
266             case 'E': // day in week (text)
267                 rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays);
268                 break;
269             case 'u': // day in week (number)
270                 rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen));
271                 break;
272             case 'D': // day in year (number)
273                 rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen);
274                 break;
275             case 'F': // day of week in month (number)
276                 rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen);
277                 break;
278             case 'w': // week in year (number)
279                 rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen);
280                 break;
281             case 'W': // week in month (number)
282                 rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen);
283                 break;
284             case 'a': // am/pm marker (text)
285                 rule = new TextField(Calendar.AM_PM, AmPmStrings);
286                 break;
287             case 'k': // hour in day (1..24)
288                 rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen));
289                 break;
290             case 'K': // hour in am/pm (0..11)
291                 rule = selectNumberRule(Calendar.HOUR, tokenLen);
292                 break;
293             case 'X': // ISO 8601
294                 rule = Iso8601_Rule.getRule(tokenLen);
295                 break;
296             case 'z': // time zone (text)
297                 if (tokenLen >= 4) {
298                     rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG);
299                 } else {
300                     rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT);
301                 }
302                 break;
303             case 'Z': // time zone (value)
304                 if (tokenLen == 1) {
305                     rule = TimeZoneNumberRule.INSTANCE_NO_COLON;
306                 } else if (tokenLen == 2) {
307                     rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES;
308                 } else {
309                     rule = TimeZoneNumberRule.INSTANCE_COLON;
310                 }
311                 break;
312             case '\'': // literal text
313                 final String sub = token.substring(1);
314                 if (sub.length() == 1) {
315                     rule = new CharacterLiteral(sub.charAt(0));
316                 } else {
317                     rule = new StringLiteral(sub);
318                 }
319                 break;
320             default:
321                 throw new IllegalArgumentException("Illegal pattern component: " + token);
322             }
323 
324             rules.add(rule);
325         }
326 
327         return rules;
328     }
329 
330     /**
331      * Performs the parsing of tokens.
332      *
333      * @param pattern  the pattern
334      * @param indexRef  index references
335      * @return parsed token
336      */
parseToken(final String pattern, final int[] indexRef)337     protected String parseToken(final String pattern, final int[] indexRef) {
338         final StringBuilder buf = new StringBuilder();
339 
340         int i = indexRef[0];
341         final int length = pattern.length();
342 
343         char c = pattern.charAt(i);
344         if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
345             // Scan a run of the same character, which indicates a time
346             // pattern.
347             buf.append(c);
348 
349             while (i + 1 < length) {
350                 final char peek = pattern.charAt(i + 1);
351                 if (peek != c) {
352                     break;
353                 }
354                 buf.append(c);
355                 i++;
356             }
357         } else {
358             // This will identify token as text.
359             buf.append('\'');
360 
361             boolean inLiteral = false;
362 
363             for (; i < length; i++) {
364                 c = pattern.charAt(i);
365 
366                 if (c == '\'') {
367                     if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
368                         // '' is treated as escaped '
369                         i++;
370                         buf.append(c);
371                     } else {
372                         inLiteral = !inLiteral;
373                     }
374                 } else if (!inLiteral &&
375                          (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
376                     i--;
377                     break;
378                 } else {
379                     buf.append(c);
380                 }
381             }
382         }
383 
384         indexRef[0] = i;
385         return buf.toString();
386     }
387 
388     /**
389      * Gets an appropriate rule for the padding required.
390      *
391      * @param field  the field to get a rule for
392      * @param padding  the padding required
393      * @return a new rule with the correct padding
394      */
selectNumberRule(final int field, final int padding)395     protected NumberRule selectNumberRule(final int field, final int padding) {
396         switch (padding) {
397         case 1:
398             return new UnpaddedNumberField(field);
399         case 2:
400             return new TwoDigitNumberField(field);
401         default:
402             return new PaddedNumberField(field, padding);
403         }
404     }
405 
406     // Format methods
407     /**
408      * Formats a {@link Date}, {@link Calendar} or
409      * {@link Long} (milliseconds) object.
410      * @deprecated Use {{@link #format(Date)}, {{@link #format(Calendar)}, {{@link #format(long)}.
411      * @param obj  the object to format
412      * @param toAppendTo  the buffer to append to
413      * @param pos  the position - ignored
414      * @return the buffer passed in
415      */
416     @Deprecated
417     @Override
format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos)418     public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
419         if (obj instanceof Date) {
420             return format((Date) obj, toAppendTo);
421         }
422         if (obj instanceof Calendar) {
423             return format((Calendar) obj, toAppendTo);
424         }
425         if (obj instanceof Long) {
426             return format(((Long) obj).longValue(), toAppendTo);
427         }
428         throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>"));
429     }
430 
431     /**
432      * Formats a {@link Date}, {@link Calendar} or
433      * {@link Long} (milliseconds) object.
434      * @since 3.5
435      * @param obj  the object to format
436      * @return The formatted value.
437      */
format(final Object obj)438     String format(final Object obj) {
439         if (obj instanceof Date) {
440             return format((Date) obj);
441         }
442         if (obj instanceof Calendar) {
443             return format((Calendar) obj);
444         }
445         if (obj instanceof Long) {
446             return format(((Long) obj).longValue());
447         }
448         throw new IllegalArgumentException("Unknown class: " + ClassUtils.getName(obj, "<null>"));
449     }
450 
451     /* (non-Javadoc)
452      * @see org.apache.commons.lang3.time.DatePrinter#format(long)
453      */
454     @Override
format(final long millis)455     public String format(final long millis) {
456         final Calendar c = newCalendar();
457         c.setTimeInMillis(millis);
458         return applyRulesToString(c);
459     }
460 
461     /**
462      * Creates a String representation of the given Calendar by applying the rules of this printer to it.
463      * @param c the Calendar to apply the rules to.
464      * @return a String representation of the given Calendar.
465      */
applyRulesToString(final Calendar c)466     private String applyRulesToString(final Calendar c) {
467         return applyRules(c, new StringBuilder(maxLengthEstimate)).toString();
468     }
469 
470     /**
471      * Creates a new Calendar instance.
472      * @return a new Calendar instance.
473      */
newCalendar()474     private Calendar newCalendar() {
475         return Calendar.getInstance(timeZone, locale);
476     }
477 
478     /* (non-Javadoc)
479      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date)
480      */
481     @Override
format(final Date date)482     public String format(final Date date) {
483         final Calendar c = newCalendar();
484         c.setTime(date);
485         return applyRulesToString(c);
486     }
487 
488     /* (non-Javadoc)
489      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar)
490      */
491     @Override
format(final Calendar calendar)492     public String format(final Calendar calendar) {
493         return format(calendar, new StringBuilder(maxLengthEstimate)).toString();
494     }
495 
496     /* (non-Javadoc)
497      * @see org.apache.commons.lang3.time.DatePrinter#format(long, StringBuffer)
498      */
499     @Override
format(final long millis, final StringBuffer buf)500     public StringBuffer format(final long millis, final StringBuffer buf) {
501         final Calendar c = newCalendar();
502         c.setTimeInMillis(millis);
503         return (StringBuffer) applyRules(c, (Appendable) buf);
504     }
505 
506     /* (non-Javadoc)
507      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, StringBuffer)
508      */
509     @Override
format(final Date date, final StringBuffer buf)510     public StringBuffer format(final Date date, final StringBuffer buf) {
511         final Calendar c = newCalendar();
512         c.setTime(date);
513         return (StringBuffer) applyRules(c, (Appendable) buf);
514     }
515 
516     /* (non-Javadoc)
517      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, StringBuffer)
518      */
519     @Override
format(final Calendar calendar, final StringBuffer buf)520     public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
521         // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
522         return format(calendar.getTime(), buf);
523     }
524 
525     /* (non-Javadoc)
526      * @see org.apache.commons.lang3.time.DatePrinter#format(long, Appendable)
527      */
528     @Override
format(final long millis, final B buf)529     public <B extends Appendable> B format(final long millis, final B buf) {
530         final Calendar c = newCalendar();
531         c.setTimeInMillis(millis);
532         return applyRules(c, buf);
533     }
534 
535     /* (non-Javadoc)
536      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Date, Appendable)
537      */
538     @Override
format(final Date date, final B buf)539     public <B extends Appendable> B format(final Date date, final B buf) {
540         final Calendar c = newCalendar();
541         c.setTime(date);
542         return applyRules(c, buf);
543     }
544 
545     /* (non-Javadoc)
546      * @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, Appendable)
547      */
548     @Override
format(Calendar calendar, final B buf)549     public <B extends Appendable> B format(Calendar calendar, final B buf) {
550         // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
551         if (!calendar.getTimeZone().equals(timeZone)) {
552             calendar = (Calendar) calendar.clone();
553             calendar.setTimeZone(timeZone);
554         }
555         return applyRules(calendar, buf);
556     }
557 
558     /**
559      * Performs the formatting by applying the rules to the
560      * specified calendar.
561      *
562      * @param calendar the calendar to format
563      * @param buf the buffer to format into
564      * @return the specified string buffer
565      *
566      * @deprecated use {@link #format(Calendar)} or {@link #format(Calendar, Appendable)}
567      */
568     @Deprecated
applyRules(final Calendar calendar, final StringBuffer buf)569     protected StringBuffer applyRules(final Calendar calendar, final StringBuffer buf) {
570         return (StringBuffer) applyRules(calendar, (Appendable) buf);
571     }
572 
573     /**
574      * Performs the formatting by applying the rules to the
575      * specified calendar.
576      *
577      * @param calendar  the calendar to format
578      * @param buf  the buffer to format into
579      * @param <B> the Appendable class type, usually StringBuilder or StringBuffer.
580      * @return the specified string buffer
581      */
applyRules(final Calendar calendar, final B buf)582     private <B extends Appendable> B applyRules(final Calendar calendar, final B buf) {
583         try {
584             for (final Rule rule : rules) {
585                 rule.appendTo(buf, calendar);
586             }
587         } catch (final IOException ioe) {
588             ExceptionUtils.rethrow(ioe);
589         }
590         return buf;
591     }
592 
593     // Accessors
594     /* (non-Javadoc)
595      * @see org.apache.commons.lang3.time.DatePrinter#getPattern()
596      */
597     @Override
getPattern()598     public String getPattern() {
599         return pattern;
600     }
601 
602     /* (non-Javadoc)
603      * @see org.apache.commons.lang3.time.DatePrinter#getTimeZone()
604      */
605     @Override
getTimeZone()606     public TimeZone getTimeZone() {
607         return timeZone;
608     }
609 
610     /* (non-Javadoc)
611      * @see org.apache.commons.lang3.time.DatePrinter#getLocale()
612      */
613     @Override
getLocale()614     public Locale getLocale() {
615         return locale;
616     }
617 
618     /**
619      * Gets an estimate for the maximum string length that the
620      * formatter will produce.
621      *
622      * <p>The actual formatted length will almost always be less than or
623      * equal to this amount.</p>
624      *
625      * @return the maximum formatted length
626      */
getMaxLengthEstimate()627     public int getMaxLengthEstimate() {
628         return maxLengthEstimate;
629     }
630 
631     // Basics
632     /**
633      * Compares two objects for equality.
634      *
635      * @param obj  the object to compare to
636      * @return {@code true} if equal
637      */
638     @Override
equals(final Object obj)639     public boolean equals(final Object obj) {
640         if (!(obj instanceof FastDatePrinter)) {
641             return false;
642         }
643         final FastDatePrinter other = (FastDatePrinter) obj;
644         return pattern.equals(other.pattern)
645             && timeZone.equals(other.timeZone)
646             && locale.equals(other.locale);
647     }
648 
649     /**
650      * Returns a hash code compatible with equals.
651      *
652      * @return a hash code compatible with equals
653      */
654     @Override
hashCode()655     public int hashCode() {
656         return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode());
657     }
658 
659     /**
660      * Gets a debugging string version of this formatter.
661      *
662      * @return a debugging string
663      */
664     @Override
toString()665     public String toString() {
666         return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]";
667     }
668 
669     // Serializing
670     /**
671      * Create the object after serialization. This implementation reinitializes the
672      * transient properties.
673      *
674      * @param in ObjectInputStream from which the object is being deserialized.
675      * @throws IOException if there is an IO issue.
676      * @throws ClassNotFoundException if a class cannot be found.
677      */
readObject(final ObjectInputStream in)678     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
679         in.defaultReadObject();
680         init();
681     }
682 
683     /**
684      * Appends two digits to the given buffer.
685      *
686      * @param buffer the buffer to append to.
687      * @param value the value to append digits from.
688      * @throws IOException If an I/O error occurs
689      */
appendDigits(final Appendable buffer, final int value)690     private static void appendDigits(final Appendable buffer, final int value) throws IOException {
691         buffer.append((char) (value / 10 + '0'));
692         buffer.append((char) (value % 10 + '0'));
693     }
694 
695     private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3
696 
697     /**
698      * Appends all digits to the given buffer.
699      *
700      * @param buffer the buffer to append to.
701      * @param value the value to append digits from.
702      * @param minFieldWidth Minimum field width.
703      * @throws IOException If an I/O error occurs
704      */
appendFullDigits(final Appendable buffer, int value, int minFieldWidth)705     private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException {
706         // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array
707         // see LANG-1248
708         if (value < 10000) {
709             // less memory allocation path works for four digits or less
710 
711             int nDigits = 4;
712             if (value < 1000) {
713                 --nDigits;
714                 if (value < 100) {
715                     --nDigits;
716                     if (value < 10) {
717                         --nDigits;
718                     }
719                 }
720             }
721             // left zero pad
722             for (int i = minFieldWidth - nDigits; i > 0; --i) {
723                 buffer.append('0');
724             }
725 
726             switch (nDigits) {
727             case 4:
728                 buffer.append((char) (value / 1000 + '0'));
729                 value %= 1000;
730             case 3:
731                 if (value >= 100) {
732                     buffer.append((char) (value / 100 + '0'));
733                     value %= 100;
734                 } else {
735                     buffer.append('0');
736                 }
737             case 2:
738                 if (value >= 10) {
739                     buffer.append((char) (value / 10 + '0'));
740                     value %= 10;
741                 } else {
742                     buffer.append('0');
743                 }
744             case 1:
745                 buffer.append((char) (value + '0'));
746             }
747         } else {
748             // more memory allocation path works for any digits
749 
750             // build up decimal representation in reverse
751             final char[] work = new char[MAX_DIGITS];
752             int digit = 0;
753             while (value != 0) {
754                 work[digit++] = (char) (value % 10 + '0');
755                 value = value / 10;
756             }
757 
758             // pad with zeros
759             while (digit < minFieldWidth) {
760                 buffer.append('0');
761                 --minFieldWidth;
762             }
763 
764             // reverse
765             while (--digit >= 0) {
766                 buffer.append(work[digit]);
767             }
768         }
769     }
770 
771     // Rules
772     /**
773      * Inner class defining a rule.
774      */
775     private interface Rule {
776         /**
777          * Returns the estimated length of the result.
778          *
779          * @return the estimated length
780          */
estimateLength()781         int estimateLength();
782 
783         /**
784          * Appends the value of the specified calendar to the output buffer based on the rule implementation.
785          *
786          * @param buf the output buffer
787          * @param calendar calendar to be appended
788          * @throws IOException if an I/O error occurs.
789          */
appendTo(Appendable buf, Calendar calendar)790         void appendTo(Appendable buf, Calendar calendar) throws IOException;
791     }
792 
793     /**
794      * Inner class defining a numeric rule.
795      */
796     private interface NumberRule extends Rule {
797         /**
798          * Appends the specified value to the output buffer based on the rule implementation.
799          *
800          * @param buffer the output buffer
801          * @param value the value to be appended
802          * @throws IOException if an I/O error occurs.
803          */
appendTo(Appendable buffer, int value)804         void appendTo(Appendable buffer, int value) throws IOException;
805     }
806 
807     /**
808      * Inner class to output a constant single character.
809      */
810     private static class CharacterLiteral implements Rule {
811         private final char mValue;
812 
813         /**
814          * Constructs a new instance of {@link CharacterLiteral}
815          * to hold the specified value.
816          *
817          * @param value the character literal
818          */
CharacterLiteral(final char value)819         CharacterLiteral(final char value) {
820             mValue = value;
821         }
822 
823         /**
824          * {@inheritDoc}
825          */
826         @Override
estimateLength()827         public int estimateLength() {
828             return 1;
829         }
830 
831         /**
832          * {@inheritDoc}
833          */
834         @Override
appendTo(final Appendable buffer, final Calendar calendar)835         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
836             buffer.append(mValue);
837         }
838     }
839 
840     /**
841      * Inner class to output a constant string.
842      */
843     private static class StringLiteral implements Rule {
844         private final String mValue;
845 
846         /**
847          * Constructs a new instance of {@link StringLiteral}
848          * to hold the specified value.
849          *
850          * @param value the string literal
851          */
StringLiteral(final String value)852         StringLiteral(final String value) {
853             mValue = value;
854         }
855 
856         /**
857          * {@inheritDoc}
858          */
859         @Override
estimateLength()860         public int estimateLength() {
861             return mValue.length();
862         }
863 
864         /**
865          * {@inheritDoc}
866          */
867         @Override
appendTo(final Appendable buffer, final Calendar calendar)868         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
869             buffer.append(mValue);
870         }
871     }
872 
873     /**
874      * Inner class to output one of a set of values.
875      */
876     private static class TextField implements Rule {
877         private final int mField;
878         private final String[] mValues;
879 
880         /**
881          * Constructs an instance of {@link TextField}
882          * with the specified field and values.
883          *
884          * @param field the field
885          * @param values the field values
886          */
TextField(final int field, final String[] values)887         TextField(final int field, final String[] values) {
888             mField = field;
889             mValues = values;
890         }
891 
892         /**
893          * {@inheritDoc}
894          */
895         @Override
estimateLength()896         public int estimateLength() {
897             int max = 0;
898             for (int i=mValues.length; --i >= 0; ) {
899                 final int len = mValues[i].length();
900                 if (len > max) {
901                     max = len;
902                 }
903             }
904             return max;
905         }
906 
907         /**
908          * {@inheritDoc}
909          */
910         @Override
appendTo(final Appendable buffer, final Calendar calendar)911         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
912             buffer.append(mValues[calendar.get(mField)]);
913         }
914     }
915 
916     /**
917      * Inner class to output an unpadded number.
918      */
919     private static class UnpaddedNumberField implements NumberRule {
920         private final int mField;
921 
922         /**
923          * Constructs an instance of {@link UnpaddedNumberField} with the specified field.
924          *
925          * @param field the field
926          */
UnpaddedNumberField(final int field)927         UnpaddedNumberField(final int field) {
928             mField = field;
929         }
930 
931         /**
932          * {@inheritDoc}
933          */
934         @Override
estimateLength()935         public int estimateLength() {
936             return 4;
937         }
938 
939         /**
940          * {@inheritDoc}
941          */
942         @Override
appendTo(final Appendable buffer, final Calendar calendar)943         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
944             appendTo(buffer, calendar.get(mField));
945         }
946 
947         /**
948          * {@inheritDoc}
949          */
950         @Override
appendTo(final Appendable buffer, final int value)951         public final void appendTo(final Appendable buffer, final int value) throws IOException {
952             if (value < 10) {
953                 buffer.append((char) (value + '0'));
954             } else if (value < 100) {
955                 appendDigits(buffer, value);
956             } else {
957                appendFullDigits(buffer, value, 1);
958             }
959         }
960     }
961 
962     /**
963      * Inner class to output an unpadded month.
964      */
965     private static class UnpaddedMonthField implements NumberRule {
966         static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField();
967 
968         /**
969          * Constructs an instance of {@link UnpaddedMonthField}.
970          *
971          */
UnpaddedMonthField()972         UnpaddedMonthField() {
973         }
974 
975         /**
976          * {@inheritDoc}
977          */
978         @Override
estimateLength()979         public int estimateLength() {
980             return 2;
981         }
982 
983         /**
984          * {@inheritDoc}
985          */
986         @Override
appendTo(final Appendable buffer, final Calendar calendar)987         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
988             appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
989         }
990 
991         /**
992          * {@inheritDoc}
993          */
994         @Override
appendTo(final Appendable buffer, final int value)995         public final void appendTo(final Appendable buffer, final int value) throws IOException {
996             if (value < 10) {
997                 buffer.append((char) (value + '0'));
998             } else {
999                 appendDigits(buffer, value);
1000             }
1001         }
1002     }
1003 
1004     /**
1005      * Inner class to output a padded number.
1006      */
1007     private static class PaddedNumberField implements NumberRule {
1008         private final int mField;
1009         private final int mSize;
1010 
1011         /**
1012          * Constructs an instance of {@link PaddedNumberField}.
1013          *
1014          * @param field the field
1015          * @param size size of the output field
1016          */
PaddedNumberField(final int field, final int size)1017         PaddedNumberField(final int field, final int size) {
1018             if (size < 3) {
1019                 // Should use UnpaddedNumberField or TwoDigitNumberField.
1020                 throw new IllegalArgumentException();
1021             }
1022             mField = field;
1023             mSize = size;
1024         }
1025 
1026         /**
1027          * {@inheritDoc}
1028          */
1029         @Override
estimateLength()1030         public int estimateLength() {
1031             return mSize;
1032         }
1033 
1034         /**
1035          * {@inheritDoc}
1036          */
1037         @Override
appendTo(final Appendable buffer, final Calendar calendar)1038         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1039             appendTo(buffer, calendar.get(mField));
1040         }
1041 
1042         /**
1043          * {@inheritDoc}
1044          */
1045         @Override
appendTo(final Appendable buffer, final int value)1046         public final void appendTo(final Appendable buffer, final int value) throws IOException {
1047             appendFullDigits(buffer, value, mSize);
1048         }
1049     }
1050 
1051     /**
1052      * Inner class to output a two digit number.
1053      */
1054     private static class TwoDigitNumberField implements NumberRule {
1055         private final int mField;
1056 
1057         /**
1058          * Constructs an instance of {@link TwoDigitNumberField} with the specified field.
1059          *
1060          * @param field the field
1061          */
TwoDigitNumberField(final int field)1062         TwoDigitNumberField(final int field) {
1063             mField = field;
1064         }
1065 
1066         /**
1067          * {@inheritDoc}
1068          */
1069         @Override
estimateLength()1070         public int estimateLength() {
1071             return 2;
1072         }
1073 
1074         /**
1075          * {@inheritDoc}
1076          */
1077         @Override
appendTo(final Appendable buffer, final Calendar calendar)1078         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1079             appendTo(buffer, calendar.get(mField));
1080         }
1081 
1082         /**
1083          * {@inheritDoc}
1084          */
1085         @Override
appendTo(final Appendable buffer, final int value)1086         public final void appendTo(final Appendable buffer, final int value) throws IOException {
1087             if (value < 100) {
1088                 appendDigits(buffer, value);
1089             } else {
1090                 appendFullDigits(buffer, value, 2);
1091             }
1092         }
1093     }
1094 
1095     /**
1096      * Inner class to output a two digit year.
1097      */
1098     private static class TwoDigitYearField implements NumberRule {
1099         static final TwoDigitYearField INSTANCE = new TwoDigitYearField();
1100 
1101         /**
1102          * Constructs an instance of {@link TwoDigitYearField}.
1103          */
TwoDigitYearField()1104         TwoDigitYearField() {
1105         }
1106 
1107         /**
1108          * {@inheritDoc}
1109          */
1110         @Override
estimateLength()1111         public int estimateLength() {
1112             return 2;
1113         }
1114 
1115         /**
1116          * {@inheritDoc}
1117          */
1118         @Override
appendTo(final Appendable buffer, final Calendar calendar)1119         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1120             appendTo(buffer, calendar.get(Calendar.YEAR) % 100);
1121         }
1122 
1123         /**
1124          * {@inheritDoc}
1125          */
1126         @Override
appendTo(final Appendable buffer, final int value)1127         public final void appendTo(final Appendable buffer, final int value) throws IOException {
1128             appendDigits(buffer, value % 100);
1129         }
1130     }
1131 
1132     /**
1133      * Inner class to output a two digit month.
1134      */
1135     private static class TwoDigitMonthField implements NumberRule {
1136         static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField();
1137 
1138         /**
1139          * Constructs an instance of {@link TwoDigitMonthField}.
1140          */
TwoDigitMonthField()1141         TwoDigitMonthField() {
1142         }
1143 
1144         /**
1145          * {@inheritDoc}
1146          */
1147         @Override
estimateLength()1148         public int estimateLength() {
1149             return 2;
1150         }
1151 
1152         /**
1153          * {@inheritDoc}
1154          */
1155         @Override
appendTo(final Appendable buffer, final Calendar calendar)1156         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1157             appendTo(buffer, calendar.get(Calendar.MONTH) + 1);
1158         }
1159 
1160         /**
1161          * {@inheritDoc}
1162          */
1163         @Override
appendTo(final Appendable buffer, final int value)1164         public final void appendTo(final Appendable buffer, final int value) throws IOException {
1165             appendDigits(buffer, value);
1166         }
1167     }
1168 
1169     /**
1170      * Inner class to output the twelve hour field.
1171      */
1172     private static class TwelveHourField implements NumberRule {
1173         private final NumberRule mRule;
1174 
1175         /**
1176          * Constructs an instance of {@link TwelveHourField} with the specified
1177          * {@link NumberRule}.
1178          *
1179          * @param rule the rule
1180          */
TwelveHourField(final NumberRule rule)1181         TwelveHourField(final NumberRule rule) {
1182             mRule = rule;
1183         }
1184 
1185         /**
1186          * {@inheritDoc}
1187          */
1188         @Override
estimateLength()1189         public int estimateLength() {
1190             return mRule.estimateLength();
1191         }
1192 
1193         /**
1194          * {@inheritDoc}
1195          */
1196         @Override
appendTo(final Appendable buffer, final Calendar calendar)1197         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1198             int value = calendar.get(Calendar.HOUR);
1199             if (value == 0) {
1200                 value = calendar.getLeastMaximum(Calendar.HOUR) + 1;
1201             }
1202             mRule.appendTo(buffer, value);
1203         }
1204 
1205         /**
1206          * {@inheritDoc}
1207          */
1208         @Override
appendTo(final Appendable buffer, final int value)1209         public void appendTo(final Appendable buffer, final int value) throws IOException {
1210             mRule.appendTo(buffer, value);
1211         }
1212     }
1213 
1214     /**
1215      * Inner class to output the twenty four hour field.
1216      */
1217     private static class TwentyFourHourField implements NumberRule {
1218         private final NumberRule mRule;
1219 
1220         /**
1221          * Constructs an instance of {@link TwentyFourHourField} with the specified
1222          * {@link NumberRule}.
1223          *
1224          * @param rule the rule
1225          */
TwentyFourHourField(final NumberRule rule)1226         TwentyFourHourField(final NumberRule rule) {
1227             mRule = rule;
1228         }
1229 
1230         /**
1231          * {@inheritDoc}
1232          */
1233         @Override
estimateLength()1234         public int estimateLength() {
1235             return mRule.estimateLength();
1236         }
1237 
1238         /**
1239          * {@inheritDoc}
1240          */
1241         @Override
appendTo(final Appendable buffer, final Calendar calendar)1242         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1243             int value = calendar.get(Calendar.HOUR_OF_DAY);
1244             if (value == 0) {
1245                 value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1;
1246             }
1247             mRule.appendTo(buffer, value);
1248         }
1249 
1250         /**
1251          * {@inheritDoc}
1252          */
1253         @Override
appendTo(final Appendable buffer, final int value)1254         public void appendTo(final Appendable buffer, final int value) throws IOException {
1255             mRule.appendTo(buffer, value);
1256         }
1257     }
1258 
1259     /**
1260      * Inner class to output the numeric day in week.
1261      */
1262     private static class DayInWeekField implements NumberRule {
1263         private final NumberRule mRule;
1264 
DayInWeekField(final NumberRule rule)1265         DayInWeekField(final NumberRule rule) {
1266             mRule = rule;
1267         }
1268 
1269         @Override
estimateLength()1270         public int estimateLength() {
1271             return mRule.estimateLength();
1272         }
1273 
1274         @Override
appendTo(final Appendable buffer, final Calendar calendar)1275         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1276             final int value = calendar.get(Calendar.DAY_OF_WEEK);
1277             mRule.appendTo(buffer, value == Calendar.SUNDAY ? 7 : value - 1);
1278         }
1279 
1280         @Override
appendTo(final Appendable buffer, final int value)1281         public void appendTo(final Appendable buffer, final int value) throws IOException {
1282             mRule.appendTo(buffer, value);
1283         }
1284     }
1285 
1286     /**
1287      * Inner class to output the numeric day in week.
1288      */
1289     private static class WeekYear implements NumberRule {
1290         private final NumberRule mRule;
1291 
WeekYear(final NumberRule rule)1292         WeekYear(final NumberRule rule) {
1293             mRule = rule;
1294         }
1295 
1296         @Override
estimateLength()1297         public int estimateLength() {
1298             return mRule.estimateLength();
1299         }
1300 
1301         @Override
appendTo(final Appendable buffer, final Calendar calendar)1302         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1303             mRule.appendTo(buffer, calendar.getWeekYear());
1304         }
1305 
1306         @Override
appendTo(final Appendable buffer, final int value)1307         public void appendTo(final Appendable buffer, final int value) throws IOException {
1308             mRule.appendTo(buffer, value);
1309         }
1310     }
1311 
1312 
1313     private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache =
1314         new ConcurrentHashMap<>(7);
1315 
1316     /**
1317      * Gets the time zone display name, using a cache for performance.
1318      *
1319      * @param tz  the zone to query
1320      * @param daylight  true if daylight savings
1321      * @param style  the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT}
1322      * @param locale  the locale to use
1323      * @return the textual name of the time zone
1324      */
getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale)1325     static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) {
1326         final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale);
1327         // This is a very slow call, so cache the results.
1328         return cTimeZoneDisplayCache.computeIfAbsent(key, k -> tz.getDisplayName(daylight, style, locale));
1329     }
1330 
1331     /**
1332      * Inner class to output a time zone name.
1333      */
1334     private static class TimeZoneNameRule implements Rule {
1335         private final Locale mLocale;
1336         private final int mStyle;
1337         private final String mStandard;
1338         private final String mDaylight;
1339 
1340         /**
1341          * Constructs an instance of {@link TimeZoneNameRule} with the specified properties.
1342          *
1343          * @param timeZone the time zone
1344          * @param locale the locale
1345          * @param style the style
1346          */
TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style)1347         TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) {
1348             mLocale = LocaleUtils.toLocale(locale);
1349             mStyle = style;
1350 
1351             mStandard = getTimeZoneDisplay(timeZone, false, style, locale);
1352             mDaylight = getTimeZoneDisplay(timeZone, true, style, locale);
1353         }
1354 
1355         /**
1356          * {@inheritDoc}
1357          */
1358         @Override
estimateLength()1359         public int estimateLength() {
1360             // We have no access to the Calendar object that will be passed to
1361             // appendTo so base estimate on the TimeZone passed to the
1362             // constructor
1363             return Math.max(mStandard.length(), mDaylight.length());
1364         }
1365 
1366         /**
1367          * {@inheritDoc}
1368          */
1369         @Override
appendTo(final Appendable buffer, final Calendar calendar)1370         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1371             final TimeZone zone = calendar.getTimeZone();
1372             if (calendar.get(Calendar.DST_OFFSET) == 0) {
1373                 buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale));
1374             } else {
1375                 buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale));
1376             }
1377         }
1378     }
1379 
1380     /**
1381      * Inner class to output a time zone as a number {@code +/-HHMM}
1382      * or {@code +/-HH:MM}.
1383      */
1384     private static class TimeZoneNumberRule implements Rule {
1385         static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true);
1386         static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false);
1387 
1388         final boolean mColon;
1389 
1390         /**
1391          * Constructs an instance of {@link TimeZoneNumberRule} with the specified properties.
1392          *
1393          * @param colon add colon between HH and MM in the output if {@code true}
1394          */
TimeZoneNumberRule(final boolean colon)1395         TimeZoneNumberRule(final boolean colon) {
1396             mColon = colon;
1397         }
1398 
1399         /**
1400          * {@inheritDoc}
1401          */
1402         @Override
estimateLength()1403         public int estimateLength() {
1404             return 5;
1405         }
1406 
1407         /**
1408          * {@inheritDoc}
1409          */
1410         @Override
appendTo(final Appendable buffer, final Calendar calendar)1411         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1412 
1413             int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
1414 
1415             if (offset < 0) {
1416                 buffer.append('-');
1417                 offset = -offset;
1418             } else {
1419                 buffer.append('+');
1420             }
1421 
1422             final int hours = offset / (60 * 60 * 1000);
1423             appendDigits(buffer, hours);
1424 
1425             if (mColon) {
1426                 buffer.append(':');
1427             }
1428 
1429             final int minutes = offset / (60 * 1000) - 60 * hours;
1430             appendDigits(buffer, minutes);
1431         }
1432     }
1433 
1434     /**
1435      * Inner class to output a time zone as a number {@code +/-HHMM}
1436      * or {@code +/-HH:MM}.
1437      */
1438     private static class Iso8601_Rule implements Rule {
1439 
1440         // Sign TwoDigitHours or Z
1441         static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3);
1442         // Sign TwoDigitHours Minutes or Z
1443         static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5);
1444         // Sign TwoDigitHours : Minutes or Z
1445         static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6);
1446 
1447         /**
1448          * Factory method for Iso8601_Rules.
1449          *
1450          * @param tokenLen a token indicating the length of the TimeZone String to be formatted.
1451          * @return an Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such
1452          *          rule exists, an IllegalArgumentException will be thrown.
1453          */
getRule(final int tokenLen)1454         static Iso8601_Rule getRule(final int tokenLen) {
1455             switch(tokenLen) {
1456             case 1:
1457                 return ISO8601_HOURS;
1458             case 2:
1459                 return ISO8601_HOURS_MINUTES;
1460             case 3:
1461                 return ISO8601_HOURS_COLON_MINUTES;
1462             default:
1463                 throw new IllegalArgumentException("invalid number of X");
1464             }
1465         }
1466 
1467         final int length;
1468 
1469         /**
1470          * Constructs an instance of {@code Iso8601_Rule} with the specified properties.
1471          *
1472          * @param length The number of characters in output (unless Z is output)
1473          */
Iso8601_Rule(final int length)1474         Iso8601_Rule(final int length) {
1475             this.length = length;
1476         }
1477 
1478         /**
1479          * {@inheritDoc}
1480          */
1481         @Override
estimateLength()1482         public int estimateLength() {
1483             return length;
1484         }
1485 
1486         /**
1487          * {@inheritDoc}
1488          */
1489         @Override
appendTo(final Appendable buffer, final Calendar calendar)1490         public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
1491             int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
1492             if (offset == 0) {
1493                 buffer.append("Z");
1494                 return;
1495             }
1496 
1497             if (offset < 0) {
1498                 buffer.append('-');
1499                 offset = -offset;
1500             } else {
1501                 buffer.append('+');
1502             }
1503 
1504             final int hours = offset / (60 * 60 * 1000);
1505             appendDigits(buffer, hours);
1506 
1507             if (length<5) {
1508                 return;
1509             }
1510 
1511             if (length==6) {
1512                 buffer.append(':');
1513             }
1514 
1515             final int minutes = offset / (60 * 1000) - 60 * hours;
1516             appendDigits(buffer, minutes);
1517         }
1518     }
1519 
1520     /**
1521      * Inner class that acts as a compound key for time zone names.
1522      */
1523     private static class TimeZoneDisplayKey {
1524         private final TimeZone mTimeZone;
1525         private final int mStyle;
1526         private final Locale mLocale;
1527 
1528         /**
1529          * Constructs an instance of {@link TimeZoneDisplayKey} with the specified properties.
1530          *
1531          * @param timeZone the time zone
1532          * @param daylight adjust the style for daylight saving time if {@code true}
1533          * @param style the time zone style
1534          * @param locale the time zone locale
1535          */
TimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale)1536         TimeZoneDisplayKey(final TimeZone timeZone,
1537                            final boolean daylight, final int style, final Locale locale) {
1538             mTimeZone = timeZone;
1539             if (daylight) {
1540                 mStyle = style | 0x80000000;
1541             } else {
1542                 mStyle = style;
1543             }
1544             mLocale = LocaleUtils.toLocale(locale);
1545         }
1546 
1547         /**
1548          * {@inheritDoc}
1549          */
1550         @Override
hashCode()1551         public int hashCode() {
1552             return (mStyle * 31 + mLocale.hashCode() ) * 31 + mTimeZone.hashCode();
1553         }
1554 
1555         /**
1556          * {@inheritDoc}
1557          */
1558         @Override
equals(final Object obj)1559         public boolean equals(final Object obj) {
1560             if (this == obj) {
1561                 return true;
1562             }
1563             if (obj instanceof TimeZoneDisplayKey) {
1564                 final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj;
1565                 return
1566                     mTimeZone.equals(other.mTimeZone) &&
1567                     mStyle == other.mStyle &&
1568                     mLocale.equals(other.mLocale);
1569             }
1570             return false;
1571         }
1572     }
1573 }
1574