/**
* $Revision $
* $Date $
*
* Copyright (C) 2005-2010 Jive Software. All rights reserved.
*
* 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.jivesoftware.community.util;
import java.io.Serializable;
import java.text.*;
import java.util.*;
public class FastDateFormat
{
public static final Object FULL = new Integer(0);
public static final Object LONG = new Integer(1);
public static final Object MEDIUM = new Integer(2);
public static final Object SHORT = new Integer(3);
private static final double LOG_10 = Math.log(10D);
private static String cDefaultPattern;
private static TimeZone cDefaultTimeZone = TimeZone.getDefault();
private static Map cTimeZoneDisplayCache = new HashMap();
private static Map cInstanceCache = new HashMap(7);
private static Map cDateInstanceCache = new HashMap(7);
private static Map cTimeInstanceCache = new HashMap(7);
private static Map cDateTimeInstanceCache = new HashMap(7);
private final String mPattern;
private final TimeZone mTimeZone;
private final Locale mLocale;
private final Rule mRules[];
private final int mMaxLengthEstimate;
private static class Pair implements Comparable, Serializable
{
public int compareTo(Object obj)
{
if(this == obj)
return 0;
Pair other = (Pair)obj;
Object a = mObj1;
Object b = other.mObj1;
if(a == null)
{
if(b != null)
return 1;
} else
{
if(b == null)
return -1;
int result = ((Comparable)a).compareTo(b);
if(result != 0)
return result;
}
a = mObj2;
b = other.mObj2;
if(a == null)
return b == null ? 0 : 1;
if(b == null)
return -1;
else
return ((Comparable)a).compareTo(b);
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if(!(obj instanceof Pair))
{
return false;
} else
{
Pair key = (Pair)obj;
return (mObj1 != null ? mObj1.equals(key.mObj1) : key.mObj1 == null) && (mObj2 != null ? mObj2.equals(key.mObj2) : key.mObj2 == null);
}
}
public int hashCode()
{
return (mObj1 != null ? mObj1.hashCode() : 0) + (mObj2 != null ? mObj2.hashCode() : 0);
}
public String toString()
{
return (new StringBuilder()).append("[").append(mObj1).append(':').append(mObj2).append(']').toString();
}
private final Object mObj1;
private final Object mObj2;
public Pair(Object obj1, Object obj2)
{
mObj1 = obj1;
mObj2 = obj2;
}
}
private static class TimeZoneDisplayKey
{
public int hashCode()
{
return mStyle * 31 + mLocale.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if(obj instanceof TimeZoneDisplayKey)
{
TimeZoneDisplayKey other = (TimeZoneDisplayKey)obj;
return mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale);
} else
{
return false;
}
}
private final TimeZone mTimeZone;
private final int mStyle;
private final Locale mLocale;
TimeZoneDisplayKey(TimeZone timeZone, boolean daylight, int style, Locale locale)
{
mTimeZone = timeZone;
if(daylight)
style |= 0x80000000;
mStyle = style;
mLocale = locale;
}
}
private static class TimeZoneRule
implements Rule
{
public int estimateLength()
{
if(mTimeZone != null)
return Math.max(mStandard.length(), mDaylight.length());
return mStyle != 0 ? 40 : 4;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
TimeZone timeZone;
if((timeZone = mTimeZone) != null)
{
if(timeZone.useDaylightTime() && calendar.get(16) != 0)
buffer.append(mDaylight);
else
buffer.append(mStandard);
} else
{
timeZone = calendar.getTimeZone();
if(timeZone.useDaylightTime() && calendar.get(16) != 0)
buffer.append(FastDateFormat.getTimeZoneDisplay(timeZone, true, mStyle, mLocale));
else
buffer.append(FastDateFormat.getTimeZoneDisplay(timeZone, false, mStyle, mLocale));
}
}
private final TimeZone mTimeZone;
private final Locale mLocale;
private final int mStyle;
private final String mStandard;
private final String mDaylight;
TimeZoneRule(TimeZone timeZone, Locale locale, int style)
{
mTimeZone = timeZone;
mLocale = locale;
mStyle = style;
if(timeZone != null)
{
mStandard = FastDateFormat.getTimeZoneDisplay(timeZone, false, style, locale);
mDaylight = FastDateFormat.getTimeZoneDisplay(timeZone, true, style, locale);
} else
{
mStandard = null;
mDaylight = null;
}
}
}
private static class TwentyFourHourField
implements NumberRule
{
public int estimateLength()
{
return mRule.estimateLength();
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
int value = calendar.get(11);
if(value == 0)
value = calendar.getMaximum(11) + 1;
mRule.appendTo(buffer, value);
}
public void appendTo(StringBuffer buffer, int value)
{
mRule.appendTo(buffer, value);
}
private final NumberRule mRule;
TwentyFourHourField(NumberRule rule)
{
mRule = rule;
}
}
private static class TwelveHourField
implements NumberRule
{
public int estimateLength()
{
return mRule.estimateLength();
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
int value = calendar.get(10);
if(value == 0)
value = calendar.getLeastMaximum(10) + 1;
mRule.appendTo(buffer, value);
}
public void appendTo(StringBuffer buffer, int value)
{
mRule.appendTo(buffer, value);
}
private final NumberRule mRule;
TwelveHourField(NumberRule rule)
{
mRule = rule;
}
}
private static class TwoDigitMonthField
implements NumberRule
{
public int estimateLength()
{
return 2;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(2) + 1);
}
public final void appendTo(StringBuffer buffer, int value)
{
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
}
TwoDigitMonthField()
{
}
}
private static class TwoDigitYearField
implements NumberRule
{
public int estimateLength()
{
return 2;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(1) % 100);
}
public final void appendTo(StringBuffer buffer, int value)
{
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
}
TwoDigitYearField()
{
}
}
private static class TwoDigitNumberField
implements NumberRule
{
public int estimateLength()
{
return 2;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(mField));
}
public final void appendTo(StringBuffer buffer, int value)
{
if(value < 100)
{
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
} else
{
buffer.append(Integer.toString(value));
}
}
private final int mField;
TwoDigitNumberField(int field)
{
mField = field;
}
}
private static class PaddedNumberField
implements NumberRule
{
public int estimateLength()
{
return 4;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(mField));
}
public final void appendTo(StringBuffer buffer, int value)
{
if(value < 100)
{
for(int i = mSize; --i >= 2;)
buffer.append('0');
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
} else
{
int digits;
if(value < 1000)
digits = 3;
else
digits = (int)(Math.log(value) / FastDateFormat.LOG_10) + 1;
for(int i = mSize; --i >= digits;)
buffer.append('0');
buffer.append(Integer.toString(value));
}
}
private final int mField;
private final int mSize;
PaddedNumberField(int field, int size)
{
if(size < 3)
{
throw new IllegalArgumentException();
} else
{
mField = field;
mSize = size;
return;
}
}
}
private static class UnpaddedMonthField
implements NumberRule
{
public int estimateLength()
{
return 2;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(2) + 1);
}
public final void appendTo(StringBuffer buffer, int value)
{
if(value < 10)
{
buffer.append((char)(value + 48));
} else
{
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
}
}
UnpaddedMonthField()
{
}
}
private static class UnpaddedNumberField
implements NumberRule
{
public int estimateLength()
{
return 4;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
appendTo(buffer, calendar.get(mField));
}
public final void appendTo(StringBuffer buffer, int value)
{
if(value < 10)
buffer.append((char)(value + 48));
else
if(value < 100)
{
buffer.append((char)(value / 10 + 48));
buffer.append((char)(value % 10 + 48));
} else
{
buffer.append(Integer.toString(value));
}
}
private final int mField;
UnpaddedNumberField(int field)
{
mField = field;
}
}
private static class TextField
implements Rule
{
public int estimateLength()
{
int max = 0;
int i = mValues.length;
do
{
if(--i < 0)
break;
int len = mValues[i].length();
if(len > max)
max = len;
} while(true);
return max;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
buffer.append(mValues[calendar.get(mField)]);
}
private final int mField;
private final String mValues[];
TextField(int field, String values[])
{
mField = field;
mValues = values;
}
}
private static class StringLiteral
implements Rule
{
public int estimateLength()
{
return mValue.length();
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
buffer.append(mValue);
}
private final String mValue;
StringLiteral(String value)
{
mValue = value;
}
}
private static class CharacterLiteral
implements Rule
{
public int estimateLength()
{
return 1;
}
public void appendTo(StringBuffer buffer, Calendar calendar)
{
buffer.append(mValue);
}
private final char mValue;
CharacterLiteral(char value)
{
mValue = value;
}
}
private static interface NumberRule
extends Rule
{
public abstract void appendTo(StringBuffer stringbuffer, int i);
}
private static interface Rule
{
public abstract int estimateLength();
public abstract void appendTo(StringBuffer stringbuffer, Calendar calendar);
}
public static FastDateFormat getInstance()
{
return getInstance(getDefaultPattern(), null, null, null);
}
public static FastDateFormat getInstance(String pattern)
throws IllegalArgumentException
{
return getInstance(pattern, null, null, null);
}
public static FastDateFormat getInstance(String pattern, TimeZone timeZone)
throws IllegalArgumentException
{
return getInstance(pattern, timeZone, null, null);
}
public static FastDateFormat getInstance(String pattern, Locale locale)
throws IllegalArgumentException
{
return getInstance(pattern, null, locale, null);
}
public static FastDateFormat getInstance(String pattern, DateFormatSymbols symbols)
throws IllegalArgumentException
{
return getInstance(pattern, null, null, symbols);
}
public static FastDateFormat getInstance(String pattern, TimeZone timeZone, Locale locale)
throws IllegalArgumentException
{
return getInstance(pattern, timeZone, locale, null);
}
public static synchronized FastDateFormat getInstance(String pattern, TimeZone timeZone, Locale locale, DateFormatSymbols symbols)
throws IllegalArgumentException
{
Object key = pattern;
if(timeZone != null)
key = new Pair(key, timeZone);
if(locale != null)
key = new Pair(key, locale);
if(symbols != null)
key = new Pair(key, symbols);
FastDateFormat format = (FastDateFormat)cInstanceCache.get(key);
if(format == null)
{
if(locale == null)
locale = Locale.getDefault();
if(symbols == null)
symbols = new DateFormatSymbols(locale);
format = new FastDateFormat(pattern, timeZone, locale, symbols);
cInstanceCache.put(key, format);
}
return format;
}
public static synchronized FastDateFormat getDateInstance(Object style, TimeZone timeZone, Locale locale)
throws IllegalArgumentException
{
Object key = style;
if(timeZone != null)
key = new Pair(key, timeZone);
if(locale == null)
key = new Pair(key, locale);
FastDateFormat format = (FastDateFormat)cDateInstanceCache.get(key);
if(format == null)
{
int ds;
try
{
ds = ((Integer)style).intValue();
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("Illegal date style: ").append(style).toString());
}
if(locale == null)
locale = Locale.getDefault();
try
{
String pattern = ((SimpleDateFormat)DateFormat.getDateInstance(ds, locale)).toPattern();
format = getInstance(pattern, timeZone, locale);
cDateInstanceCache.put(key, format);
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("No date pattern for locale: ").append(locale).toString());
}
}
return format;
}
public static synchronized FastDateFormat getTimeInstance(Object style, TimeZone timeZone, Locale locale)
throws IllegalArgumentException
{
Object key = style;
if(timeZone != null)
key = new Pair(key, timeZone);
if(locale != null)
key = new Pair(key, locale);
FastDateFormat format = (FastDateFormat)cTimeInstanceCache.get(key);
if(format == null)
{
int ts;
try
{
ts = ((Integer)style).intValue();
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("Illegal time style: ").append(style).toString());
}
if(locale == null)
locale = Locale.getDefault();
try
{
String pattern = ((SimpleDateFormat)DateFormat.getTimeInstance(ts, locale)).toPattern();
format = getInstance(pattern, timeZone, locale);
cTimeInstanceCache.put(key, format);
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("No date pattern for locale: ").append(locale).toString());
}
}
return format;
}
public static synchronized FastDateFormat getDateTimeInstance(Object dateStyle, Object timeStyle, TimeZone timeZone, Locale locale)
throws IllegalArgumentException
{
Object key = new Pair(dateStyle, timeStyle);
if(timeZone != null)
key = new Pair(key, timeZone);
if(locale != null)
key = new Pair(key, locale);
FastDateFormat format = (FastDateFormat)cDateTimeInstanceCache.get(key);
if(format == null)
{
int ds;
try
{
ds = ((Integer)dateStyle).intValue();
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("Illegal date style: ").append(dateStyle).toString());
}
int ts;
try
{
ts = ((Integer)timeStyle).intValue();
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("Illegal time style: ").append(timeStyle).toString());
}
if(locale == null)
locale = Locale.getDefault();
try
{
String pattern = ((SimpleDateFormat)DateFormat.getDateTimeInstance(ds, ts, locale)).toPattern();
format = getInstance(pattern, timeZone, locale);
cDateTimeInstanceCache.put(key, format);
}
catch(ClassCastException e)
{
throw new IllegalArgumentException((new StringBuilder()).append("No date time pattern for locale: ").append(locale).toString());
}
}
return format;
}
static synchronized String getTimeZoneDisplay(TimeZone tz, boolean daylight, int style, Locale locale)
{
Object key = new TimeZoneDisplayKey(tz, daylight, style, locale);
String value = (String)cTimeZoneDisplayCache.get(key);
if(value == null)
{
value = tz.getDisplayName(daylight, style, locale);
cTimeZoneDisplayCache.put(key, value);
}
return value;
}
private static synchronized String getDefaultPattern()
{
if(cDefaultPattern == null)
cDefaultPattern = (new SimpleDateFormat()).toPattern();
return cDefaultPattern;
}
private static List parse(String pattern, TimeZone timeZone, Locale locale, DateFormatSymbols symbols)
{
List rules = new ArrayList();
String ERAs[] = symbols.getEras();
String months[] = symbols.getMonths();
String shortMonths[] = symbols.getShortMonths();
String weekdays[] = symbols.getWeekdays();
String shortWeekdays[] = symbols.getShortWeekdays();
String AmPmStrings[] = symbols.getAmPmStrings();
int length = pattern.length();
int indexRef[] = new int[1];
int i = 0;
do
{
if(i >= length)
break;
indexRef[0] = i;
String token = parseToken(pattern, indexRef);
i = indexRef[0];
int tokenLen = token.length();
if(tokenLen == 0)
break;
char c = token.charAt(0);
Rule rule;
switch(c)
{
case 71: // 'G'
rule = new TextField(0, ERAs);
break;
case 121: // 'y'
if(tokenLen >= 4)
rule = new UnpaddedNumberField(1);
else
rule = new TwoDigitYearField();
break;
case 77: // 'M'
if(tokenLen >= 4)
{
rule = new TextField(2, months);
break;
}
if(tokenLen == 3)
{
rule = new TextField(2, shortMonths);
break;
}
if(tokenLen == 2)
rule = new TwoDigitMonthField();
else
rule = new UnpaddedMonthField();
break;
case 100: // 'd'
rule = selectNumberRule(5, tokenLen);
break;
case 104: // 'h'
rule = new TwelveHourField(selectNumberRule(10, tokenLen));
break;
case 72: // 'H'
rule = selectNumberRule(11, tokenLen);
break;
case 109: // 'm'
rule = selectNumberRule(12, tokenLen);
break;
case 115: // 's'
rule = selectNumberRule(13, tokenLen);
break;
case 83: // 'S'
rule = selectNumberRule(14, tokenLen);
break;
case 69: // 'E'
rule = new TextField(7, tokenLen >= 4 ? weekdays : shortWeekdays);
break;
case 68: // 'D'
rule = selectNumberRule(6, tokenLen);
break;
case 70: // 'F'
rule = selectNumberRule(8, tokenLen);
break;
case 119: // 'w'
rule = selectNumberRule(3, tokenLen);
break;
case 87: // 'W'
rule = selectNumberRule(4, tokenLen);
break;
case 97: // 'a'
rule = new TextField(9, AmPmStrings);
break;
case 107: // 'k'
rule = new TwentyFourHourField(selectNumberRule(11, tokenLen));
break;
case 75: // 'K'
rule = selectNumberRule(10, tokenLen);
break;
case 122: // 'z'
if(tokenLen >= 4)
rule = new TimeZoneRule(timeZone, locale, 1);
else
rule = new TimeZoneRule(timeZone, locale, 0);
break;
case 39: // '\''
String sub = token.substring(1);
if(sub.length() == 1)
rule = new CharacterLiteral(sub.charAt(0));
else
rule = new StringLiteral(new String(sub));
break;
case 40: // '('
case 41: // ')'
case 42: // '*'
case 43: // '+'
case 44: // ','
case 45: // '-'
case 46: // '.'
case 47: // '/'
case 48: // '0'
case 49: // '1'
case 50: // '2'
case 51: // '3'
case 52: // '4'
case 53: // '5'
case 54: // '6'
case 55: // '7'
case 56: // '8'
case 57: // '9'
case 58: // ':'
case 59: // ';'
case 60: // '<'
case 61: // '='
case 62: // '>'
case 63: // '?'
case 64: // '@'
case 65: // 'A'
case 66: // 'B'
case 67: // 'C'
case 73: // 'I'
case 74: // 'J'
case 76: // 'L'
case 78: // 'N'
case 79: // 'O'
case 80: // 'P'
case 81: // 'Q'
case 82: // 'R'
case 84: // 'T'
case 85: // 'U'
case 86: // 'V'
case 88: // 'X'
case 89: // 'Y'
case 90: // 'Z'
case 91: // '['
case 92: // '\\'
case 93: // ']'
case 94: // '^'
case 95: // '_'
case 96: // '`'
case 98: // 'b'
case 99: // 'c'
case 101: // 'e'
case 102: // 'f'
case 103: // 'g'
case 105: // 'i'
case 106: // 'j'
case 108: // 'l'
case 110: // 'n'
case 111: // 'o'
case 112: // 'p'
case 113: // 'q'
case 114: // 'r'
case 116: // 't'
case 117: // 'u'
case 118: // 'v'
case 120: // 'x'
default:
throw new IllegalArgumentException((new StringBuilder()).append("Illegal pattern component: ").append(token).toString());
}
rules.add(rule);
i++;
} while(true);
return rules;
}
private static String parseToken(String pattern, int indexRef[])
{
StringBuffer buf = new StringBuffer();
int i = indexRef[0];
int length = pattern.length();
char c = pattern.charAt(i);
if(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')
{
buf.append(c);
do
{
if(i + 1 >= length)
break;
char peek = pattern.charAt(i + 1);
if(peek != c)
break;
buf.append(c);
i++;
} while(true);
} else
{
buf.append('\'');
boolean inLiteral = false;
for(; i < length; i++)
{
c = pattern.charAt(i);
if(c == '\'')
{
if(i + 1 < length && pattern.charAt(i + 1) == '\'')
{
i++;
buf.append(c);
} else
{
inLiteral = !inLiteral;
}
continue;
}
if(!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'))
{
i--;
break;
}
buf.append(c);
}
}
indexRef[0] = i;
return buf.toString();
}
private static NumberRule selectNumberRule(int field, int padding)
{
switch(padding)
{
case 1: // '\001'
return new UnpaddedNumberField(field);
case 2: // '\002'
return new TwoDigitNumberField(field);
}
return new PaddedNumberField(field, padding);
}
private FastDateFormat()
{
this(getDefaultPattern(), null, null, null);
}
private FastDateFormat(String pattern)
throws IllegalArgumentException
{
this(pattern, null, null, null);
}
private FastDateFormat(String pattern, TimeZone timeZone)
throws IllegalArgumentException
{
this(pattern, timeZone, null, null);
}
private FastDateFormat(String pattern, Locale locale)
throws IllegalArgumentException
{
this(pattern, null, locale, null);
}
private FastDateFormat(String pattern, DateFormatSymbols symbols)
throws IllegalArgumentException
{
this(pattern, null, null, symbols);
}
private FastDateFormat(String pattern, TimeZone timeZone, Locale locale)
throws IllegalArgumentException
{
this(pattern, timeZone, locale, null);
}
private FastDateFormat(String pattern, TimeZone timeZone, Locale locale, DateFormatSymbols symbols)
throws IllegalArgumentException
{
if(locale == null)
locale = Locale.getDefault();
mPattern = pattern;
mTimeZone = timeZone;
mLocale = locale;
if(symbols == null)
symbols = new DateFormatSymbols(locale);
List rulesList = parse(pattern, timeZone, locale, symbols);
mRules = (Rule[])(Rule[])rulesList.toArray(new Rule[rulesList.size()]);
int len = 0;
for(int i = mRules.length; --i >= 0;)
len += mRules[i].estimateLength();
mMaxLengthEstimate = len;
}
public String format(Date date)
{
Calendar c = new GregorianCalendar(cDefaultTimeZone);
c.setTime(date);
if(mTimeZone != null)
c.setTimeZone(mTimeZone);
return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString();
}
public String format(Calendar calendar)
{
return format(calendar, new StringBuffer(mMaxLengthEstimate)).toString();
}
public StringBuffer format(Date date, StringBuffer buf)
{
Calendar c = new GregorianCalendar(cDefaultTimeZone);
c.setTime(date);
if(mTimeZone != null)
c.setTimeZone(mTimeZone);
return applyRules(c, buf);
}
public StringBuffer format(Calendar calendar, StringBuffer buf)
{
if(mTimeZone != null)
{
calendar = (Calendar)calendar.clone();
calendar.setTimeZone(mTimeZone);
}
return applyRules(calendar, buf);
}
private StringBuffer applyRules(Calendar calendar, StringBuffer buf)
{
Rule rules[] = mRules;
int len = mRules.length;
for(int i = 0; i < len; i++)
rules[i].appendTo(buf, calendar);
return buf;
}
public String getPattern()
{
return mPattern;
}
public TimeZone getTimeZone()
{
return mTimeZone;
}
public Locale getLocale()
{
return mLocale;
}
public int getMaxLengthEstimate()
{
return mMaxLengthEstimate;
}
}