org.joda.time.format.DateTimeFormat Maven / Gradle / Ivy
Show all versions of virtdata-lib-realer Show documentation
/*
* Copyright 2001-2014 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.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReferenceArray;
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.
*
* 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}.
*
* This class provides two types of factory:
*
* - {@link #forPattern(String) Pattern} provides a DateTimeFormatter based on
* a pattern string that is mostly compatible with the JDK date patterns.
*
- {@link #forStyle(String) Style} provides a DateTimeFormatter based on a
* two character style, representing short, medium, long and full.
*
*
* For example, to use a pattern:
*
* DateTime dt = new DateTime();
* DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM, yyyy");
* String str = fmt.print(dt);
*
*
* 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:
*
*
* 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 millis 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 '
*
*
* The count of pattern letters determine the format.
*
* Text: 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.
*
* Number: The minimum number of digits.
* Shorter numbers are zero-padded to this amount.
* When parsing, any number of digits are accepted.
*
* Year: 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.
*
* Month: 3 or over, use text, otherwise use number.
*
* Millis: The exact number of fractional digits.
* If more millisecond digits are available then specified the number will be truncated,
* if there are fewer than specified then the number will be zero-padded to the right.
* When parsing, only the exact number of digits are accepted.
*
* Zone: 'Z' outputs offset without a colon, 'ZZ' outputs
* the offset with a colon, 'ZZZ' or more outputs the zone id.
*
* Zone names: Time zone names ('z') cannot be parsed.
*
* 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.
*
* 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;
/** Maximum size of the pattern cache. */
private static final int PATTERN_CACHE_SIZE = 500;
/** Maps patterns to formatters, patterns don't vary by locale. Size capped at PATTERN_CACHE_SIZE*/
private static final ConcurrentHashMap cPatternCache = new ConcurrentHashMap();
/** Maps patterns to formatters, patterns don't vary by locale. */
private static final AtomicReferenceArray cStyleCache = new AtomicReferenceArray(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.
*
* 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:
*
* DateTimeFormat.forPattern(pattern).withLocale(Locale.FRANCE).print(dt);
*
*
* @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.
*
* 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 omitted by specifying a style character '-'.
*
* 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:
*
* DateTimeFormat.forStyle(style).withLocale(Locale.FRANCE).print(dt);
*
*
* @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.
*
* 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 omitted 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.getPrinter0()).getPattern(locale);
}
//-----------------------------------------------------------------------
/**
* Creates a format that outputs a short date format.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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.
*
* 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= 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 = cPatternCache.get(pattern);
if (formatter == null) {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
parsePatternTo(builder, pattern);
formatter = builder.toFormatter();
if (cPatternCache.size() < PATTERN_CACHE_SIZE) {
// the size check is not locked against concurrent access,
// but is accepted to be slightly off in contention scenarios.
DateTimeFormatter oldFormatter = cPatternCache.putIfAbsent(pattern, formatter);
if (oldFormatter != null) {
formatter = oldFormatter;
}
}
}
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 omitted 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; // (dateStyle * 5 + timeStyle);
// Should never happen but do a double check...
if (index >= cStyleCache.length()) {
return createDateTimeFormatter(dateStyle, timeStyle);
}
DateTimeFormatter f = cStyleCache.get(index);
if (f == null) {
f = createDateTimeFormatter(dateStyle, timeStyle);
if (cStyleCache.compareAndSet(index, null, f) == false) {
f = cStyleCache.get(index);
}
}
return f;
}
/**
* Creates a formatter for the specified style.
*
* @param dateStyle the date style
* @param timeStyle the time style
* @return the formatter
*/
private static DateTimeFormatter createDateTimeFormatter(int dateStyle, int timeStyle){
int type = DATETIME;
if (dateStyle == NONE) {
type = TIME;
} else if (timeStyle == NONE) {
type = DATE;
}
StyleFormatter llf = new StyleFormatter(dateStyle, timeStyle, type);
return new DateTimeFormatter(llf, llf);
}
/**
* 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 InternalPrinter, InternalParser {
private static final ConcurrentHashMap cCache = new ConcurrentHashMap();
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(
Appendable appenadble, long instant, Chronology chrono,
int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException {
InternalPrinter p = getFormatter(locale).getPrinter0();
p.printTo(appenadble, instant, chrono, displayOffset, displayZone, locale);
}
public void printTo(Appendable appendable, ReadablePartial partial, Locale locale) throws IOException {
InternalPrinter p = getFormatter(locale).getPrinter0();
p.printTo(appendable, partial, locale);
}
public int estimateParsedLength() {
return 40; // guess
}
public int parseInto(DateTimeParserBucket bucket, CharSequence text, int position) {
InternalParser p = getFormatter(bucket.getLocale()).getParser0();
return p.parseInto(bucket, text, position);
}
private DateTimeFormatter getFormatter(Locale locale) {
locale = (locale == null ? Locale.getDefault() : locale);
StyleFormatterCacheKey key = new StyleFormatterCacheKey(iType, iDateStyle, iTimeStyle, locale);
DateTimeFormatter f = cCache.get(key);
if (f == null) {
f = DateTimeFormat.forPattern(getPattern(locale));
DateTimeFormatter oldFormatter = cCache.putIfAbsent(key, f);
if (oldFormatter != null) {
f = oldFormatter;
}
}
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();
}
}
static class StyleFormatterCacheKey {
private final int combinedTypeAndStyle;
private final Locale locale;
public StyleFormatterCacheKey(int iType, int iDateStyle,
int iTimeStyle, Locale locale) {
this.locale = locale;
// keeping old key generation logic of shifting type and style
this.combinedTypeAndStyle = iType + (iDateStyle << 4) + (iTimeStyle << 8);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + combinedTypeAndStyle;
result = prime * result + ((locale == null) ? 0 : locale.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof StyleFormatterCacheKey)) {
return false;
}
StyleFormatterCacheKey other = (StyleFormatterCacheKey) obj;
if (combinedTypeAndStyle != other.combinedTypeAndStyle) {
return false;
}
if (locale == null) {
if (other.locale != null) {
return false;
}
} else if (!locale.equals(other.locale)) {
return false;
}
return true;
}
}
}