/* * Copyright 2001-2009 Stephen Colebourne * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.joda.time.format; import java.io.IOException; import java.io.Writer; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Locale; import java.util.Map; import org.joda.time.Chronology; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.ReadablePartial; /** * Factory that creates instances of DateTimeFormatter from patterns and styles. * <p> * Datetime formatting is performed by the {@link DateTimeFormatter} class. * Three classes provide factory methods to create formatters, and this is one. * The others are {@link ISODateTimeFormat} and {@link DateTimeFormatterBuilder}. * <p> * This class provides two types of factory: * <ul> * <li>{@link #forPattern(String) Pattern} provides a DateTimeFormatter based on * a pattern string that is mostly compatible with the JDK date patterns. * <li>{@link #forStyle(String) Style} provides a DateTimeFormatter based on a * two character style, representing short, medium, long and full. * </ul> * <p> * For example, to use a patterm: * <pre> * DateTime dt = new DateTime(); * DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM, yyyy"); * String str = fmt.print(dt); * </pre> * * The pattern syntax is mostly compatible with java.text.SimpleDateFormat - * time zone names cannot be parsed and a few more symbols are supported. * All ASCII letters are reserved as pattern letters, which are defined as follows: * <blockquote> * <pre> * Symbol Meaning Presentation Examples * ------ ------- ------------ ------- * G era text AD * C century of era (>=0) number 20 * Y year of era (>=0) year 1996 * * x weekyear year 1996 * w week of weekyear number 27 * e day of week number 2 * E day of week text Tuesday; Tue * * y year year 1996 * D day of year number 189 * M month of year month July; Jul; 07 * d day of month number 10 * * a halfday of day text PM * K hour of halfday (0~11) number 0 * h clockhour of halfday (1~12) number 12 * * H hour of day (0~23) number 0 * k clockhour of day (1~24) number 24 * m minute of hour number 30 * s second of minute number 55 * S fraction of second number 978 * * z time zone text Pacific Standard Time; PST * Z time zone offset/id zone -0800; -08:00; America/Los_Angeles * * ' escape for text delimiter * '' single quote literal ' * </pre> * </blockquote> * The count of pattern letters determine the format. * <p> * <strong>Text</strong>: If the number of pattern letters is 4 or more, * the full form is used; otherwise a short or abbreviated form is used if * available. * <p> * <strong>Number</strong>: The minimum number of digits. Shorter numbers * are zero-padded to this amount. * <p> * <strong>Year</strong>: Numeric presentation for year and weekyear fields * are handled specially. For example, if the count of 'y' is 2, the year * will be displayed as the zero-based year of the century, which is two * digits. * <p> * <strong>Month</strong>: 3 or over, use text, otherwise use number. * <p> * <strong>Zone</strong>: 'Z' outputs offset without a colon, 'ZZ' outputs * the offset with a colon, 'ZZZ' or more outputs the zone id. * <p> * <strong>Zone names</strong>: Time zone names ('z') cannot be parsed. * <p> * Any characters in the pattern that are not in the ranges of ['a'..'z'] * and ['A'..'Z'] will be treated as quoted text. For instance, characters * like ':', '.', ' ', '#' and '?' will appear in the resulting time text * even they are not embraced within single quotes. * <p> * DateTimeFormat is thread-safe and immutable, and the formatters it returns * are as well. * * @author Brian S O'Neill * @author Maxim Zhao * @since 1.0 * @see ISODateTimeFormat * @see DateTimeFormatterBuilder */ public class DateTimeFormat { /** Style constant for FULL. */ static final int FULL = 0; // DateFormat.FULL /** Style constant for LONG. */ static final int LONG = 1; // DateFormat.LONG /** Style constant for MEDIUM. */ static final int MEDIUM = 2; // DateFormat.MEDIUM /** Style constant for SHORT. */ static final int SHORT = 3; // DateFormat.SHORT /** Style constant for NONE. */ static final int NONE = 4; /** Type constant for DATE only. */ static final int DATE = 0; /** Type constant for TIME only. */ static final int TIME = 1; /** Type constant for DATETIME. */ static final int DATETIME = 2; /** Maps patterns to formatters, patterns don't vary by locale. */ private static final Map<String, DateTimeFormatter> cPatternedCache = new HashMap<String, DateTimeFormatter>(7); /** Maps patterns to formatters, patterns don't vary by locale. */ private static final DateTimeFormatter[] cStyleCache = new DateTimeFormatter[25]; //----------------------------------------------------------------------- /** * Factory to create a formatter from a pattern string. * The pattern string is described above in the class level javadoc. * It is very similar to SimpleDateFormat patterns. * <p> * The format may contain locale specific output, and this will change as * you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * For example: * <pre> * DateTimeFormat.forPattern(pattern).withLocale(Locale.FRANCE).print(dt); * </pre> * * @param pattern pattern specification * @return the formatter * @throws IllegalArgumentException if the pattern is invalid */ public static DateTimeFormatter forPattern(String pattern) { return createFormatterForPattern(pattern); } /** * Factory to create a format from a two character style pattern. * <p> * The first character is the date style, and the second character is the * time style. Specify a character of 'S' for short style, 'M' for medium, * 'L' for long, and 'F' for full. * A date or time may be ommitted by specifying a style character '-'. * <p> * The returned formatter will dynamically adjust to the locale that * the print/parse takes place in. Thus you just call * {@link DateTimeFormatter#withLocale(Locale)} and the Short/Medium/Long/Full * style for that locale will be output. For example: * <pre> * DateTimeFormat.forStyle(style).withLocale(Locale.FRANCE).print(dt); * </pre> * * @param style two characters from the set {"S", "M", "L", "F", "-"} * @return the formatter * @throws IllegalArgumentException if the style is invalid */ public static DateTimeFormatter forStyle(String style) { return createFormatterForStyle(style); } /** * Returns the pattern used by a particular style and locale. * <p> * The first character is the date style, and the second character is the * time style. Specify a character of 'S' for short style, 'M' for medium, * 'L' for long, and 'F' for full. * A date or time may be ommitted by specifying a style character '-'. * * @param style two characters from the set {"S", "M", "L", "F", "-"} * @param locale locale to use, null means default * @return the formatter * @throws IllegalArgumentException if the style is invalid * @since 1.3 */ public static String patternForStyle(String style, Locale locale) { DateTimeFormatter formatter = createFormatterForStyle(style); if (locale == null) { locale = Locale.getDefault(); } // Not pretty, but it works. return ((StyleFormatter) formatter.getPrinter()).getPattern(locale); } //----------------------------------------------------------------------- /** * Creates a format that outputs a short date format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter shortDate() { return createFormatterForStyleIndex(SHORT, NONE); } /** * Creates a format that outputs a short time format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter shortTime() { return createFormatterForStyleIndex(NONE, SHORT); } /** * Creates a format that outputs a short datetime format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter shortDateTime() { return createFormatterForStyleIndex(SHORT, SHORT); } //----------------------------------------------------------------------- /** * Creates a format that outputs a medium date format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter mediumDate() { return createFormatterForStyleIndex(MEDIUM, NONE); } /** * Creates a format that outputs a medium time format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter mediumTime() { return createFormatterForStyleIndex(NONE, MEDIUM); } /** * Creates a format that outputs a medium datetime format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter mediumDateTime() { return createFormatterForStyleIndex(MEDIUM, MEDIUM); } //----------------------------------------------------------------------- /** * Creates a format that outputs a long date format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter longDate() { return createFormatterForStyleIndex(LONG, NONE); } /** * Creates a format that outputs a long time format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter longTime() { return createFormatterForStyleIndex(NONE, LONG); } /** * Creates a format that outputs a long datetime format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter longDateTime() { return createFormatterForStyleIndex(LONG, LONG); } //----------------------------------------------------------------------- /** * Creates a format that outputs a full date format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter fullDate() { return createFormatterForStyleIndex(FULL, NONE); } /** * Creates a format that outputs a full time format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter fullTime() { return createFormatterForStyleIndex(NONE, FULL); } /** * Creates a format that outputs a full datetime format. * <p> * The format will change as you change the locale of the formatter. * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. * * @return the formatter */ public static DateTimeFormatter fullDateTime() { return createFormatterForStyleIndex(FULL, FULL); } //----------------------------------------------------------------------- /** * Parses the given pattern and appends the rules to the given * DateTimeFormatterBuilder. * * @param pattern pattern specification * @throws IllegalArgumentException if the pattern is invalid */ static void appendPatternTo(DateTimeFormatterBuilder builder, String pattern) { parsePatternTo(builder, pattern); } //----------------------------------------------------------------------- /** * Constructor. * * @since 1.1 (previously private) */ protected DateTimeFormat() { super(); } //----------------------------------------------------------------------- /** * Parses the given pattern and appends the rules to the given * DateTimeFormatterBuilder. * * @param pattern pattern specification * @throws IllegalArgumentException if the pattern is invalid * @see #forPattern */ private static void parsePatternTo(DateTimeFormatterBuilder builder, String pattern) { int length = pattern.length(); int[] indexRef = new int[1]; for (int i=0; i<length; i++) { indexRef[0] = i; String token = parseToken(pattern, indexRef); i = indexRef[0]; int tokenLen = token.length(); if (tokenLen == 0) { break; } char c = token.charAt(0); switch (c) { case 'G': // era designator (text) builder.appendEraText(); break; case 'C': // century of era (number) builder.appendCenturyOfEra(tokenLen, tokenLen); break; case 'x': // weekyear (number) case 'y': // year (number) case 'Y': // year of era (number) if (tokenLen == 2) { boolean lenientParse = true; // Peek ahead to next token. if (i + 1 < length) { indexRef[0]++; if (isNumericToken(parseToken(pattern, indexRef))) { // If next token is a number, cannot support // lenient parse, because it will consume digits // that it should not. lenientParse = false; } indexRef[0]--; } // Use pivots which are compatible with SimpleDateFormat. switch (c) { case 'x': builder.appendTwoDigitWeekyear (new DateTime().getWeekyear() - 30, lenientParse); break; case 'y': case 'Y': default: builder.appendTwoDigitYear(new DateTime().getYear() - 30, lenientParse); break; } } else { // Try to support long year values. int maxDigits = 9; // Peek ahead to next token. if (i + 1 < length) { indexRef[0]++; if (isNumericToken(parseToken(pattern, indexRef))) { // If next token is a number, cannot support long years. maxDigits = tokenLen; } indexRef[0]--; } switch (c) { case 'x': builder.appendWeekyear(tokenLen, maxDigits); break; case 'y': builder.appendYear(tokenLen, maxDigits); break; case 'Y': builder.appendYearOfEra(tokenLen, maxDigits); break; } } break; case 'M': // month of year (text and number) if (tokenLen >= 3) { if (tokenLen >= 4) { builder.appendMonthOfYearText(); } else { builder.appendMonthOfYearShortText(); } } else { builder.appendMonthOfYear(tokenLen); } break; case 'd': // day of month (number) builder.appendDayOfMonth(tokenLen); break; case 'a': // am/pm marker (text) builder.appendHalfdayOfDayText(); break; case 'h': // clockhour of halfday (number, 1..12) builder.appendClockhourOfHalfday(tokenLen); break; case 'H': // hour of day (number, 0..23) builder.appendHourOfDay(tokenLen); break; case 'k': // clockhour of day (1..24) builder.appendClockhourOfDay(tokenLen); break; case 'K': // hour of halfday (0..11) builder.appendHourOfHalfday(tokenLen); break; case 'm': // minute of hour (number) builder.appendMinuteOfHour(tokenLen); break; case 's': // second of minute (number) builder.appendSecondOfMinute(tokenLen); break; case 'S': // fraction of second (number) builder.appendFractionOfSecond(tokenLen, tokenLen); break; case 'e': // day of week (number) builder.appendDayOfWeek(tokenLen); break; case 'E': // dayOfWeek (text) if (tokenLen >= 4) { builder.appendDayOfWeekText(); } else { builder.appendDayOfWeekShortText(); } break; case 'D': // day of year (number) builder.appendDayOfYear(tokenLen); break; case 'w': // week of weekyear (number) builder.appendWeekOfWeekyear(tokenLen); break; case 'z': // time zone (text) if (tokenLen >= 4) { builder.appendTimeZoneName(); } else { builder.appendTimeZoneShortName(null); } break; case 'Z': // time zone offset if (tokenLen == 1) { builder.appendTimeZoneOffset(null, "Z", false, 2, 2); } else if (tokenLen == 2) { builder.appendTimeZoneOffset(null, "Z", true, 2, 2); } else { builder.appendTimeZoneId(); } break; case '\'': // literal text String sub = token.substring(1); if (sub.length() == 1) { builder.appendLiteral(sub.charAt(0)); } else { // Create copy of sub since otherwise the temporary quoted // string would still be referenced internally. builder.appendLiteral(new String(sub)); } break; default: throw new IllegalArgumentException ("Illegal pattern component: " + token); } } } /** * Parses an individual token. * * @param pattern the pattern string * @param indexRef a single element array, where the input is the start * location and the output is the location after parsing the token * @return the parsed token */ private static String parseToken(String pattern, int[] indexRef) { StringBuilder buf = new StringBuilder(); int i = indexRef[0]; int length = pattern.length(); char c = pattern.charAt(i); if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { // Scan a run of the same character, which indicates a time // pattern. buf.append(c); while (i + 1 < length) { char peek = pattern.charAt(i + 1); if (peek == c) { buf.append(c); i++; } else { break; } } } else { // This will identify token as text. buf.append('\''); boolean inLiteral = false; for (; i < length; i++) { c = pattern.charAt(i); if (c == '\'') { if (i + 1 < length && pattern.charAt(i + 1) == '\'') { // '' is treated as escaped ' i++; buf.append(c); } else { inLiteral = !inLiteral; } } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { i--; break; } else { buf.append(c); } } } indexRef[0] = i; return buf.toString(); } /** * Returns true if token should be parsed as a numeric field. * * @param token the token to parse * @return true if numeric field */ private static boolean isNumericToken(String token) { int tokenLen = token.length(); if (tokenLen > 0) { char c = token.charAt(0); switch (c) { case 'c': // century (number) case 'C': // century of era (number) case 'x': // weekyear (number) case 'y': // year (number) case 'Y': // year of era (number) case 'd': // day of month (number) case 'h': // hour of day (number, 1..12) case 'H': // hour of day (number, 0..23) case 'm': // minute of hour (number) case 's': // second of minute (number) case 'S': // fraction of second (number) case 'e': // day of week (number) case 'D': // day of year (number) case 'F': // day of week in month (number) case 'w': // week of year (number) case 'W': // week of month (number) case 'k': // hour of day (1..24) case 'K': // hour of day (0..11) return true; case 'M': // month of year (text and number) if (tokenLen <= 2) { return true; } } } return false; } //----------------------------------------------------------------------- /** * Select a format from a custom pattern. * * @param pattern pattern specification * @throws IllegalArgumentException if the pattern is invalid * @see #appendPatternTo */ private static DateTimeFormatter createFormatterForPattern(String pattern) { if (pattern == null || pattern.length() == 0) { throw new IllegalArgumentException("Invalid pattern specification"); } DateTimeFormatter formatter = null; synchronized (cPatternedCache) { formatter = cPatternedCache.get(pattern); if (formatter == null) { DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); parsePatternTo(builder, pattern); formatter = builder.toFormatter(); cPatternedCache.put(pattern, formatter); } } return formatter; } /** * Select a format from a two character style pattern. The first character * is the date style, and the second character is the time style. Specify a * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' * for full. A date or time may be ommitted by specifying a style character '-'. * * @param style two characters from the set {"S", "M", "L", "F", "-"} * @throws IllegalArgumentException if the style is invalid */ private static DateTimeFormatter createFormatterForStyle(String style) { if (style == null || style.length() != 2) { throw new IllegalArgumentException("Invalid style specification: " + style); } int dateStyle = selectStyle(style.charAt(0)); int timeStyle = selectStyle(style.charAt(1)); if (dateStyle == NONE && timeStyle == NONE) { throw new IllegalArgumentException("Style '--' is invalid"); } return createFormatterForStyleIndex(dateStyle, timeStyle); } /** * Gets the formatter for the specified style. * * @param dateStyle the date style * @param timeStyle the time style * @return the formatter */ private static DateTimeFormatter createFormatterForStyleIndex(int dateStyle, int timeStyle) { int index = ((dateStyle << 2) + dateStyle) + timeStyle; DateTimeFormatter f = null; synchronized (cStyleCache) { f = cStyleCache[index]; if (f == null) { int type = DATETIME; if (dateStyle == NONE) { type = TIME; } else if (timeStyle == NONE) { type = DATE; } StyleFormatter llf = new StyleFormatter( dateStyle, timeStyle, type); f = new DateTimeFormatter(llf, llf); cStyleCache[index] = f; } } return f; } /** * Gets the JDK style code from the Joda code. * * @param ch the Joda style code * @return the JDK style code */ private static int selectStyle(char ch) { switch (ch) { case 'S': return SHORT; case 'M': return MEDIUM; case 'L': return LONG; case 'F': return FULL; case '-': return NONE; default: throw new IllegalArgumentException("Invalid style character: " + ch); } } //----------------------------------------------------------------------- static class StyleFormatter implements DateTimePrinter, DateTimeParser { private static final Map<String, DateTimeFormatter> cCache = new HashMap<String, DateTimeFormatter>(); // manual sync private final int iDateStyle; private final int iTimeStyle; private final int iType; StyleFormatter(int dateStyle, int timeStyle, int type) { super(); iDateStyle = dateStyle; iTimeStyle = timeStyle; iType = type; } public int estimatePrintedLength() { return 40; // guess } public void printTo( StringBuffer buf, long instant, Chronology chrono, int displayOffset, DateTimeZone displayZone, Locale locale) { DateTimePrinter p = getFormatter(locale).getPrinter(); p.printTo(buf, instant, chrono, displayOffset, displayZone, locale); } public void printTo( Writer out, long instant, Chronology chrono, int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException { DateTimePrinter p = getFormatter(locale).getPrinter(); p.printTo(out, instant, chrono, displayOffset, displayZone, locale); } public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) { DateTimePrinter p = getFormatter(locale).getPrinter(); p.printTo(buf, partial, locale); } public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException { DateTimePrinter p = getFormatter(locale).getPrinter(); p.printTo(out, partial, locale); } public int estimateParsedLength() { return 40; // guess } public int parseInto(DateTimeParserBucket bucket, String text, int position) { DateTimeParser p = getFormatter(bucket.getLocale()).getParser(); return p.parseInto(bucket, text, position); } private DateTimeFormatter getFormatter(Locale locale) { locale = (locale == null ? Locale.getDefault() : locale); String key = Integer.toString(iType + (iDateStyle << 4) + (iTimeStyle << 8)) + locale.toString(); DateTimeFormatter f = null; synchronized (cCache) { f = cCache.get(key); if (f == null) { String pattern = getPattern(locale); f = DateTimeFormat.forPattern(pattern); cCache.put(key, f); } } return f; } String getPattern(Locale locale) { DateFormat f = null; switch (iType) { case DATE: f = DateFormat.getDateInstance(iDateStyle, locale); break; case TIME: f = DateFormat.getTimeInstance(iTimeStyle, locale); break; case DATETIME: f = DateFormat.getDateTimeInstance(iDateStyle, iTimeStyle, locale); break; } if (f instanceof SimpleDateFormat == false) { throw new IllegalArgumentException("No datetime pattern for locale: " + locale); } return ((SimpleDateFormat) f).toPattern(); } } }