View Javadoc

1   /*
2    *  Copyright 2001-2009 Stephen Colebourne
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.joda.time.format;
17  
18  import java.io.IOException;
19  import java.io.Writer;
20  import java.text.DateFormat;
21  import java.text.SimpleDateFormat;
22  import java.util.HashMap;
23  import java.util.Locale;
24  import java.util.Map;
25  
26  import org.joda.time.Chronology;
27  import org.joda.time.DateTime;
28  import org.joda.time.DateTimeZone;
29  import org.joda.time.ReadablePartial;
30  
31  /**
32   * Factory that creates instances of DateTimeFormatter from patterns and styles.
33   * <p>
34   * Datetime formatting is performed by the {@link DateTimeFormatter} class.
35   * Three classes provide factory methods to create formatters, and this is one.
36   * The others are {@link ISODateTimeFormat} and {@link DateTimeFormatterBuilder}.
37   * <p>
38   * This class provides two types of factory:
39   * <ul>
40   * <li>{@link #forPattern(String) Pattern} provides a DateTimeFormatter based on
41   * a pattern string that is mostly compatible with the JDK date patterns.
42   * <li>{@link #forStyle(String) Style} provides a DateTimeFormatter based on a
43   * two character style, representing short, medium, long and full.
44   * </ul>
45   * <p>
46   * For example, to use a patterm:
47   * <pre>
48   * DateTime dt = new DateTime();
49   * DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM, yyyy");
50   * String str = fmt.print(dt);
51   * </pre>
52   *
53   * The pattern syntax is mostly compatible with java.text.SimpleDateFormat -
54   * time zone names cannot be parsed and a few more symbols are supported.
55   * All ASCII letters are reserved as pattern letters, which are defined as follows:
56   * <blockquote>
57   * <pre>
58   * Symbol  Meaning                      Presentation  Examples
59   * ------  -------                      ------------  -------
60   * G       era                          text          AD
61   * C       century of era (&gt;=0)         number        20
62   * Y       year of era (&gt;=0)            year          1996
63   *
64   * x       weekyear                     year          1996
65   * w       week of weekyear             number        27
66   * e       day of week                  number        2
67   * E       day of week                  text          Tuesday; Tue
68   *
69   * y       year                         year          1996
70   * D       day of year                  number        189
71   * M       month of year                month         July; Jul; 07
72   * d       day of month                 number        10
73   *
74   * a       halfday of day               text          PM
75   * K       hour of halfday (0~11)       number        0
76   * h       clockhour of halfday (1~12)  number        12
77   *
78   * H       hour of day (0~23)           number        0
79   * k       clockhour of day (1~24)      number        24
80   * m       minute of hour               number        30
81   * s       second of minute             number        55
82   * S       fraction of second           number        978
83   *
84   * z       time zone                    text          Pacific Standard Time; PST
85   * Z       time zone offset/id          zone          -0800; -08:00; America/Los_Angeles
86   *
87   * '       escape for text              delimiter
88   * ''      single quote                 literal       '
89   * </pre>
90   * </blockquote>
91   * The count of pattern letters determine the format.
92   * <p>
93   * <strong>Text</strong>: If the number of pattern letters is 4 or more,
94   * the full form is used; otherwise a short or abbreviated form is used if
95   * available.
96   * <p>
97   * <strong>Number</strong>: The minimum number of digits. Shorter numbers
98   * are zero-padded to this amount.
99   * <p>
100  * <strong>Year</strong>: Numeric presentation for year and weekyear fields
101  * are handled specially. For example, if the count of 'y' is 2, the year
102  * will be displayed as the zero-based year of the century, which is two
103  * digits.
104  * <p>
105  * <strong>Month</strong>: 3 or over, use text, otherwise use number.
106  * <p>
107  * <strong>Zone</strong>: 'Z' outputs offset without a colon, 'ZZ' outputs
108  * the offset with a colon, 'ZZZ' or more outputs the zone id.
109  * <p>
110  * <strong>Zone names</strong>: Time zone names ('z') cannot be parsed.
111  * <p>
112  * Any characters in the pattern that are not in the ranges of ['a'..'z']
113  * and ['A'..'Z'] will be treated as quoted text. For instance, characters
114  * like ':', '.', ' ', '#' and '?' will appear in the resulting time text
115  * even they are not embraced within single quotes.
116  * <p>
117  * DateTimeFormat is thread-safe and immutable, and the formatters it returns
118  * are as well.
119  *
120  * @author Brian S O'Neill
121  * @author Maxim Zhao
122  * @since 1.0
123  * @see ISODateTimeFormat
124  * @see DateTimeFormatterBuilder
125  */
126 public class DateTimeFormat {
127 
128     /** Style constant for FULL. */
129     static final int FULL = 0;  // DateFormat.FULL
130     /** Style constant for LONG. */
131     static final int LONG = 1;  // DateFormat.LONG
132     /** Style constant for MEDIUM. */
133     static final int MEDIUM = 2;  // DateFormat.MEDIUM
134     /** Style constant for SHORT. */
135     static final int SHORT = 3;  // DateFormat.SHORT
136     /** Style constant for NONE. */
137     static final int NONE = 4;
138 
139     /** Type constant for DATE only. */
140     static final int DATE = 0;
141     /** Type constant for TIME only. */
142     static final int TIME = 1;
143     /** Type constant for DATETIME. */
144     static final int DATETIME = 2;
145 
146     /** Maps patterns to formatters, patterns don't vary by locale. */
147     private static final Map<String, DateTimeFormatter> cPatternedCache = new HashMap<String, DateTimeFormatter>(7);
148     /** Maps patterns to formatters, patterns don't vary by locale. */
149     private static final DateTimeFormatter[] cStyleCache = new DateTimeFormatter[25];
150 
151     //-----------------------------------------------------------------------
152     /**
153      * Factory to create a formatter from a pattern string.
154      * The pattern string is described above in the class level javadoc.
155      * It is very similar to SimpleDateFormat patterns.
156      * <p>
157      * The format may contain locale specific output, and this will change as
158      * you change the locale of the formatter.
159      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
160      * For example:
161      * <pre>
162      * DateTimeFormat.forPattern(pattern).withLocale(Locale.FRANCE).print(dt);
163      * </pre>
164      *
165      * @param pattern  pattern specification
166      * @return the formatter
167      * @throws IllegalArgumentException if the pattern is invalid
168      */
169     public static DateTimeFormatter forPattern(String pattern) {
170         return createFormatterForPattern(pattern);
171     }
172 
173     /**
174      * Factory to create a format from a two character style pattern.
175      * <p>
176      * The first character is the date style, and the second character is the
177      * time style. Specify a character of 'S' for short style, 'M' for medium,
178      * 'L' for long, and 'F' for full.
179      * A date or time may be ommitted by specifying a style character '-'.
180      * <p>
181      * The returned formatter will dynamically adjust to the locale that
182      * the print/parse takes place in. Thus you just call
183      * {@link DateTimeFormatter#withLocale(Locale)} and the Short/Medium/Long/Full
184      * style for that locale will be output. For example:
185      * <pre>
186      * DateTimeFormat.forStyle(style).withLocale(Locale.FRANCE).print(dt);
187      * </pre>
188      *
189      * @param style  two characters from the set {"S", "M", "L", "F", "-"}
190      * @return the formatter
191      * @throws IllegalArgumentException if the style is invalid
192      */
193     public static DateTimeFormatter forStyle(String style) {
194         return createFormatterForStyle(style);
195     }
196 
197     /**
198      * Returns the pattern used by a particular style and locale.
199      * <p>
200      * The first character is the date style, and the second character is the
201      * time style. Specify a character of 'S' for short style, 'M' for medium,
202      * 'L' for long, and 'F' for full.
203      * A date or time may be ommitted by specifying a style character '-'.
204      *
205      * @param style  two characters from the set {"S", "M", "L", "F", "-"}
206      * @param locale  locale to use, null means default
207      * @return the formatter
208      * @throws IllegalArgumentException if the style is invalid
209      * @since 1.3
210      */
211     public static String patternForStyle(String style, Locale locale) {
212         DateTimeFormatter formatter = createFormatterForStyle(style);
213         if (locale == null) {
214             locale = Locale.getDefault();
215         }
216         // Not pretty, but it works.
217         return ((StyleFormatter) formatter.getPrinter()).getPattern(locale);
218     }
219 
220     //-----------------------------------------------------------------------
221     /**
222      * Creates a format that outputs a short date format.
223      * <p>
224      * The format will change as you change the locale of the formatter.
225      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
226      * 
227      * @return the formatter
228      */
229     public static DateTimeFormatter shortDate() {
230         return createFormatterForStyleIndex(SHORT, NONE);
231     }
232 
233     /**
234      * Creates a format that outputs a short time format.
235      * <p>
236      * The format will change as you change the locale of the formatter.
237      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
238      * 
239      * @return the formatter
240      */
241     public static DateTimeFormatter shortTime() {
242         return createFormatterForStyleIndex(NONE, SHORT);
243     }
244 
245     /**
246      * Creates a format that outputs a short datetime format.
247      * <p>
248      * The format will change as you change the locale of the formatter.
249      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
250      * 
251      * @return the formatter
252      */
253     public static DateTimeFormatter shortDateTime() {
254         return createFormatterForStyleIndex(SHORT, SHORT);
255     }
256 
257     //-----------------------------------------------------------------------
258     /**
259      * Creates a format that outputs a medium date format.
260      * <p>
261      * The format will change as you change the locale of the formatter.
262      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
263      * 
264      * @return the formatter
265      */
266     public static DateTimeFormatter mediumDate() {
267         return createFormatterForStyleIndex(MEDIUM, NONE);
268     }
269 
270     /**
271      * Creates a format that outputs a medium time format.
272      * <p>
273      * The format will change as you change the locale of the formatter.
274      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
275      * 
276      * @return the formatter
277      */
278     public static DateTimeFormatter mediumTime() {
279         return createFormatterForStyleIndex(NONE, MEDIUM);
280     }
281 
282     /**
283      * Creates a format that outputs a medium datetime format.
284      * <p>
285      * The format will change as you change the locale of the formatter.
286      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
287      * 
288      * @return the formatter
289      */
290     public static DateTimeFormatter mediumDateTime() {
291         return createFormatterForStyleIndex(MEDIUM, MEDIUM);
292     }
293 
294     //-----------------------------------------------------------------------
295     /**
296      * Creates a format that outputs a long date format.
297      * <p>
298      * The format will change as you change the locale of the formatter.
299      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
300      * 
301      * @return the formatter
302      */
303     public static DateTimeFormatter longDate() {
304         return createFormatterForStyleIndex(LONG, NONE);
305     }
306 
307     /**
308      * Creates a format that outputs a long time format.
309      * <p>
310      * The format will change as you change the locale of the formatter.
311      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
312      * 
313      * @return the formatter
314      */
315     public static DateTimeFormatter longTime() {
316         return createFormatterForStyleIndex(NONE, LONG);
317     }
318 
319     /**
320      * Creates a format that outputs a long datetime format.
321      * <p>
322      * The format will change as you change the locale of the formatter.
323      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
324      * 
325      * @return the formatter
326      */
327     public static DateTimeFormatter longDateTime() {
328         return createFormatterForStyleIndex(LONG, LONG);
329     }
330 
331     //-----------------------------------------------------------------------
332     /**
333      * Creates a format that outputs a full date format.
334      * <p>
335      * The format will change as you change the locale of the formatter.
336      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
337      * 
338      * @return the formatter
339      */
340     public static DateTimeFormatter fullDate() {
341         return createFormatterForStyleIndex(FULL, NONE);
342     }
343 
344     /**
345      * Creates a format that outputs a full time format.
346      * <p>
347      * The format will change as you change the locale of the formatter.
348      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
349      * 
350      * @return the formatter
351      */
352     public static DateTimeFormatter fullTime() {
353         return createFormatterForStyleIndex(NONE, FULL);
354     }
355 
356     /**
357      * Creates a format that outputs a full datetime format.
358      * <p>
359      * The format will change as you change the locale of the formatter.
360      * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale.
361      * 
362      * @return the formatter
363      */
364     public static DateTimeFormatter fullDateTime() {
365         return createFormatterForStyleIndex(FULL, FULL);
366     }
367 
368     //-----------------------------------------------------------------------
369     /**
370      * Parses the given pattern and appends the rules to the given
371      * DateTimeFormatterBuilder.
372      *
373      * @param pattern  pattern specification
374      * @throws IllegalArgumentException if the pattern is invalid
375      */
376     static void appendPatternTo(DateTimeFormatterBuilder builder, String pattern) {
377         parsePatternTo(builder, pattern);
378     }
379 
380     //-----------------------------------------------------------------------
381     /**
382      * Constructor.
383      *
384      * @since 1.1 (previously private)
385      */
386     protected DateTimeFormat() {
387         super();
388     }
389 
390     //-----------------------------------------------------------------------
391     /**
392      * Parses the given pattern and appends the rules to the given
393      * DateTimeFormatterBuilder.
394      *
395      * @param pattern  pattern specification
396      * @throws IllegalArgumentException if the pattern is invalid
397      * @see #forPattern
398      */
399     private static void parsePatternTo(DateTimeFormatterBuilder builder, String pattern) {
400         int length = pattern.length();
401         int[] indexRef = new int[1];
402 
403         for (int i=0; i<length; i++) {
404             indexRef[0] = i;
405             String token = parseToken(pattern, indexRef);
406             i = indexRef[0];
407 
408             int tokenLen = token.length();
409             if (tokenLen == 0) {
410                 break;
411             }
412             char c = token.charAt(0);
413 
414             switch (c) {
415             case 'G': // era designator (text)
416                 builder.appendEraText();
417                 break;
418             case 'C': // century of era (number)
419                 builder.appendCenturyOfEra(tokenLen, tokenLen);
420                 break;
421             case 'x': // weekyear (number)
422             case 'y': // year (number)
423             case 'Y': // year of era (number)
424                 if (tokenLen == 2) {
425                     boolean lenientParse = true;
426 
427                     // Peek ahead to next token.
428                     if (i + 1 < length) {
429                         indexRef[0]++;
430                         if (isNumericToken(parseToken(pattern, indexRef))) {
431                             // If next token is a number, cannot support
432                             // lenient parse, because it will consume digits
433                             // that it should not.
434                             lenientParse = false;
435                         }
436                         indexRef[0]--;
437                     }
438 
439                     // Use pivots which are compatible with SimpleDateFormat.
440                     switch (c) {
441                     case 'x':
442                         builder.appendTwoDigitWeekyear
443                             (new DateTime().getWeekyear() - 30, lenientParse);
444                         break;
445                     case 'y':
446                     case 'Y':
447                     default:
448                         builder.appendTwoDigitYear(new DateTime().getYear() - 30, lenientParse);
449                         break;
450                     }
451                 } else {
452                     // Try to support long year values.
453                     int maxDigits = 9;
454 
455                     // Peek ahead to next token.
456                     if (i + 1 < length) {
457                         indexRef[0]++;
458                         if (isNumericToken(parseToken(pattern, indexRef))) {
459                             // If next token is a number, cannot support long years.
460                             maxDigits = tokenLen;
461                         }
462                         indexRef[0]--;
463                     }
464 
465                     switch (c) {
466                     case 'x':
467                         builder.appendWeekyear(tokenLen, maxDigits);
468                         break;
469                     case 'y':
470                         builder.appendYear(tokenLen, maxDigits);
471                         break;
472                     case 'Y':
473                         builder.appendYearOfEra(tokenLen, maxDigits);
474                         break;
475                     }
476                 }
477                 break;
478             case 'M': // month of year (text and number)
479                 if (tokenLen >= 3) {
480                     if (tokenLen >= 4) {
481                         builder.appendMonthOfYearText();
482                     } else {
483                         builder.appendMonthOfYearShortText();
484                     }
485                 } else {
486                     builder.appendMonthOfYear(tokenLen);
487                 }
488                 break;
489             case 'd': // day of month (number)
490                 builder.appendDayOfMonth(tokenLen);
491                 break;
492             case 'a': // am/pm marker (text)
493                 builder.appendHalfdayOfDayText();
494                 break;
495             case 'h': // clockhour of halfday (number, 1..12)
496                 builder.appendClockhourOfHalfday(tokenLen);
497                 break;
498             case 'H': // hour of day (number, 0..23)
499                 builder.appendHourOfDay(tokenLen);
500                 break;
501             case 'k': // clockhour of day (1..24)
502                 builder.appendClockhourOfDay(tokenLen);
503                 break;
504             case 'K': // hour of halfday (0..11)
505                 builder.appendHourOfHalfday(tokenLen);
506                 break;
507             case 'm': // minute of hour (number)
508                 builder.appendMinuteOfHour(tokenLen);
509                 break;
510             case 's': // second of minute (number)
511                 builder.appendSecondOfMinute(tokenLen);
512                 break;
513             case 'S': // fraction of second (number)
514                 builder.appendFractionOfSecond(tokenLen, tokenLen);
515                 break;
516             case 'e': // day of week (number)
517                 builder.appendDayOfWeek(tokenLen);
518                 break;
519             case 'E': // dayOfWeek (text)
520                 if (tokenLen >= 4) {
521                     builder.appendDayOfWeekText();
522                 } else {
523                     builder.appendDayOfWeekShortText();
524                 }
525                 break;
526             case 'D': // day of year (number)
527                 builder.appendDayOfYear(tokenLen);
528                 break;
529             case 'w': // week of weekyear (number)
530                 builder.appendWeekOfWeekyear(tokenLen);
531                 break;
532             case 'z': // time zone (text)
533                 if (tokenLen >= 4) {
534                     builder.appendTimeZoneName();
535                 } else {
536                     builder.appendTimeZoneShortName(null);
537                 }
538                 break;
539             case 'Z': // time zone offset
540                 if (tokenLen == 1) {
541                     builder.appendTimeZoneOffset(null, "Z", false, 2, 2);
542                 } else if (tokenLen == 2) {
543                     builder.appendTimeZoneOffset(null, "Z", true, 2, 2);
544                 } else {
545                     builder.appendTimeZoneId();
546                 }
547                 break;
548             case '\'': // literal text
549                 String sub = token.substring(1);
550                 if (sub.length() == 1) {
551                     builder.appendLiteral(sub.charAt(0));
552                 } else {
553                     // Create copy of sub since otherwise the temporary quoted
554                     // string would still be referenced internally.
555                     builder.appendLiteral(new String(sub));
556                 }
557                 break;
558             default:
559                 throw new IllegalArgumentException
560                     ("Illegal pattern component: " + token);
561             }
562         }
563     }
564 
565     /**
566      * Parses an individual token.
567      * 
568      * @param pattern  the pattern string
569      * @param indexRef  a single element array, where the input is the start
570      *  location and the output is the location after parsing the token
571      * @return the parsed token
572      */
573     private static String parseToken(String pattern, int[] indexRef) {
574         StringBuilder buf = new StringBuilder();
575 
576         int i = indexRef[0];
577         int length = pattern.length();
578 
579         char c = pattern.charAt(i);
580         if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
581             // Scan a run of the same character, which indicates a time
582             // pattern.
583             buf.append(c);
584 
585             while (i + 1 < length) {
586                 char peek = pattern.charAt(i + 1);
587                 if (peek == c) {
588                     buf.append(c);
589                     i++;
590                 } else {
591                     break;
592                 }
593             }
594         } else {
595             // This will identify token as text.
596             buf.append('\'');
597 
598             boolean inLiteral = false;
599 
600             for (; i < length; i++) {
601                 c = pattern.charAt(i);
602                 
603                 if (c == '\'') {
604                     if (i + 1 < length && pattern.charAt(i + 1) == '\'') {
605                         // '' is treated as escaped '
606                         i++;
607                         buf.append(c);
608                     } else {
609                         inLiteral = !inLiteral;
610                     }
611                 } else if (!inLiteral &&
612                            (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) {
613                     i--;
614                     break;
615                 } else {
616                     buf.append(c);
617                 }
618             }
619         }
620 
621         indexRef[0] = i;
622         return buf.toString();
623     }
624 
625     /**
626      * Returns true if token should be parsed as a numeric field.
627      * 
628      * @param token  the token to parse
629      * @return true if numeric field
630      */
631     private static boolean isNumericToken(String token) {
632         int tokenLen = token.length();
633         if (tokenLen > 0) {
634             char c = token.charAt(0);
635             switch (c) {
636             case 'c': // century (number)
637             case 'C': // century of era (number)
638             case 'x': // weekyear (number)
639             case 'y': // year (number)
640             case 'Y': // year of era (number)
641             case 'd': // day of month (number)
642             case 'h': // hour of day (number, 1..12)
643             case 'H': // hour of day (number, 0..23)
644             case 'm': // minute of hour (number)
645             case 's': // second of minute (number)
646             case 'S': // fraction of second (number)
647             case 'e': // day of week (number)
648             case 'D': // day of year (number)
649             case 'F': // day of week in month (number)
650             case 'w': // week of year (number)
651             case 'W': // week of month (number)
652             case 'k': // hour of day (1..24)
653             case 'K': // hour of day (0..11)
654                 return true;
655             case 'M': // month of year (text and number)
656                 if (tokenLen <= 2) {
657                     return true;
658                 }
659             }
660         }
661             
662         return false;
663     }
664 
665     //-----------------------------------------------------------------------
666     /**
667      * Select a format from a custom pattern.
668      *
669      * @param pattern  pattern specification
670      * @throws IllegalArgumentException if the pattern is invalid
671      * @see #appendPatternTo
672      */
673     private static DateTimeFormatter createFormatterForPattern(String pattern) {
674         if (pattern == null || pattern.length() == 0) {
675             throw new IllegalArgumentException("Invalid pattern specification");
676         }
677         DateTimeFormatter formatter = null;
678         synchronized (cPatternedCache) {
679             formatter = cPatternedCache.get(pattern);
680             if (formatter == null) {
681                 DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
682                 parsePatternTo(builder, pattern);
683                 formatter = builder.toFormatter();
684 
685                 cPatternedCache.put(pattern, formatter);
686             }
687         }
688         return formatter;
689     }
690 
691     /**
692      * Select a format from a two character style pattern. The first character
693      * is the date style, and the second character is the time style. Specify a
694      * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F'
695      * for full. A date or time may be ommitted by specifying a style character '-'.
696      *
697      * @param style  two characters from the set {"S", "M", "L", "F", "-"}
698      * @throws IllegalArgumentException if the style is invalid
699      */
700     private static DateTimeFormatter createFormatterForStyle(String style) {
701         if (style == null || style.length() != 2) {
702             throw new IllegalArgumentException("Invalid style specification: " + style);
703         }
704         int dateStyle = selectStyle(style.charAt(0));
705         int timeStyle = selectStyle(style.charAt(1));
706         if (dateStyle == NONE && timeStyle == NONE) {
707             throw new IllegalArgumentException("Style '--' is invalid");
708         }
709         return createFormatterForStyleIndex(dateStyle, timeStyle);
710     }
711 
712     /**
713      * Gets the formatter for the specified style.
714      * 
715      * @param dateStyle  the date style
716      * @param timeStyle  the time style
717      * @return the formatter
718      */
719     private static DateTimeFormatter createFormatterForStyleIndex(int dateStyle, int timeStyle) {
720         int index = ((dateStyle << 2) + dateStyle) + timeStyle;
721         DateTimeFormatter f = null;
722         synchronized (cStyleCache) {
723             f = cStyleCache[index];
724             if (f == null) {
725                 int type = DATETIME;
726                 if (dateStyle == NONE) {
727                     type = TIME;
728                 } else if (timeStyle == NONE) {
729                     type = DATE;
730                 }
731                 StyleFormatter llf = new StyleFormatter(
732                         dateStyle, timeStyle, type);
733                 f = new DateTimeFormatter(llf, llf);
734                 cStyleCache[index] = f;
735             }
736         }
737         return f;
738     }
739 
740     /**
741      * Gets the JDK style code from the Joda code.
742      * 
743      * @param ch  the Joda style code
744      * @return the JDK style code
745      */
746     private static int selectStyle(char ch) {
747         switch (ch) {
748         case 'S':
749             return SHORT;
750         case 'M':
751             return MEDIUM;
752         case 'L':
753             return LONG;
754         case 'F':
755             return FULL;
756         case '-':
757             return NONE;
758         default:
759             throw new IllegalArgumentException("Invalid style character: " + ch);
760         }
761     }
762 
763     //-----------------------------------------------------------------------
764     static class StyleFormatter
765             implements DateTimePrinter, DateTimeParser {
766 
767         private static final Map<String, DateTimeFormatter> cCache = new HashMap<String, DateTimeFormatter>();  // manual sync
768         
769         private final int iDateStyle;
770         private final int iTimeStyle;
771         private final int iType;
772 
773         StyleFormatter(int dateStyle, int timeStyle, int type) {
774             super();
775             iDateStyle = dateStyle;
776             iTimeStyle = timeStyle;
777             iType = type;
778         }
779 
780         public int estimatePrintedLength() {
781             return 40;  // guess
782         }
783 
784         public void printTo(
785                 StringBuffer buf, long instant, Chronology chrono,
786                 int displayOffset, DateTimeZone displayZone, Locale locale) {
787             DateTimePrinter p = getFormatter(locale).getPrinter();
788             p.printTo(buf, instant, chrono, displayOffset, displayZone, locale);
789         }
790 
791         public void printTo(
792                 Writer out, long instant, Chronology chrono,
793                 int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
794             DateTimePrinter p = getFormatter(locale).getPrinter();
795             p.printTo(out, instant, chrono, displayOffset, displayZone, locale);
796         }
797 
798         public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
799             DateTimePrinter p = getFormatter(locale).getPrinter();
800             p.printTo(buf, partial, locale);
801         }
802 
803         public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException {
804             DateTimePrinter p = getFormatter(locale).getPrinter();
805             p.printTo(out, partial, locale);
806         }
807 
808         public int estimateParsedLength() {
809             return 40;  // guess
810         }
811 
812         public int parseInto(DateTimeParserBucket bucket, String text, int position) {
813             DateTimeParser p = getFormatter(bucket.getLocale()).getParser();
814             return p.parseInto(bucket, text, position);
815         }
816 
817         private DateTimeFormatter getFormatter(Locale locale) {
818             locale = (locale == null ? Locale.getDefault() : locale);
819             String key = Integer.toString(iType + (iDateStyle << 4) + (iTimeStyle << 8)) + locale.toString();
820             DateTimeFormatter f = null;
821             synchronized (cCache) {
822                 f = cCache.get(key);
823                 if (f == null) {
824                     String pattern = getPattern(locale);
825                     f = DateTimeFormat.forPattern(pattern);
826                     cCache.put(key, f);
827                 }
828             }
829             return f;
830         }
831 
832         String getPattern(Locale locale) {
833             DateFormat f = null;
834             switch (iType) {
835                 case DATE:
836                     f = DateFormat.getDateInstance(iDateStyle, locale);
837                     break;
838                 case TIME:
839                     f = DateFormat.getTimeInstance(iTimeStyle, locale);
840                     break;
841                 case DATETIME:
842                     f = DateFormat.getDateTimeInstance(iDateStyle, iTimeStyle, locale);
843                     break;
844             }
845             if (f instanceof SimpleDateFormat == false) {
846                 throw new IllegalArgumentException("No datetime pattern for locale: " + locale);
847             }
848             return ((SimpleDateFormat) f).toPattern();
849         }
850     }
851 
852 }