001    /*
002     *  Copyright 2001-2009 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.time.format;
017    
018    import java.io.IOException;
019    import java.io.Writer;
020    import java.text.DateFormat;
021    import java.text.SimpleDateFormat;
022    import java.util.HashMap;
023    import java.util.Locale;
024    import java.util.Map;
025    
026    import org.joda.time.Chronology;
027    import org.joda.time.DateTime;
028    import org.joda.time.DateTimeZone;
029    import org.joda.time.ReadablePartial;
030    
031    /**
032     * Factory that creates instances of DateTimeFormatter from patterns and styles.
033     * <p>
034     * Datetime formatting is performed by the {@link DateTimeFormatter} class.
035     * Three classes provide factory methods to create formatters, and this is one.
036     * The others are {@link ISODateTimeFormat} and {@link DateTimeFormatterBuilder}.
037     * <p>
038     * This class provides two types of factory:
039     * <ul>
040     * <li>{@link #forPattern(String) Pattern} provides a DateTimeFormatter based on
041     * a pattern string that is mostly compatible with the JDK date patterns.
042     * <li>{@link #forStyle(String) Style} provides a DateTimeFormatter based on a
043     * two character style, representing short, medium, long and full.
044     * </ul>
045     * <p>
046     * For example, to use a patterm:
047     * <pre>
048     * DateTime dt = new DateTime();
049     * DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM, yyyy");
050     * String str = fmt.print(dt);
051     * </pre>
052     *
053     * The pattern syntax is mostly compatible with java.text.SimpleDateFormat -
054     * time zone names cannot be parsed and a few more symbols are supported.
055     * All ASCII letters are reserved as pattern letters, which are defined as follows:
056     * <blockquote>
057     * <pre>
058     * Symbol  Meaning                      Presentation  Examples
059     * ------  -------                      ------------  -------
060     * G       era                          text          AD
061     * C       century of era (&gt;=0)         number        20
062     * Y       year of era (&gt;=0)            year          1996
063     *
064     * x       weekyear                     year          1996
065     * w       week of weekyear             number        27
066     * e       day of week                  number        2
067     * E       day of week                  text          Tuesday; Tue
068     *
069     * y       year                         year          1996
070     * D       day of year                  number        189
071     * M       month of year                month         July; Jul; 07
072     * d       day of month                 number        10
073     *
074     * a       halfday of day               text          PM
075     * K       hour of halfday (0~11)       number        0
076     * h       clockhour of halfday (1~12)  number        12
077     *
078     * H       hour of day (0~23)           number        0
079     * k       clockhour of day (1~24)      number        24
080     * m       minute of hour               number        30
081     * s       second of minute             number        55
082     * S       fraction of second           number        978
083     *
084     * z       time zone                    text          Pacific Standard Time; PST
085     * Z       time zone offset/id          zone          -0800; -08:00; America/Los_Angeles
086     *
087     * '       escape for text              delimiter
088     * ''      single quote                 literal       '
089     * </pre>
090     * </blockquote>
091     * The count of pattern letters determine the format.
092     * <p>
093     * <strong>Text</strong>: If the number of pattern letters is 4 or more,
094     * the full form is used; otherwise a short or abbreviated form is used if
095     * available.
096     * <p>
097     * <strong>Number</strong>: The minimum number of digits. Shorter numbers
098     * are zero-padded to this amount.
099     * <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    }