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 */
016package org.joda.time.format;
017
018import java.io.IOException;
019import java.io.Writer;
020import java.text.DateFormat;
021import java.text.SimpleDateFormat;
022import java.util.HashMap;
023import java.util.Locale;
024import java.util.Map;
025
026import org.joda.time.Chronology;
027import org.joda.time.DateTime;
028import org.joda.time.DateTimeZone;
029import 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 */
126public 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}