LANG-949 and LANG-950

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@1557882 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Chas Honton 2014-01-13 23:01:23 +00:00
parent 2abe2c8366
commit 03645a1ec1
3 changed files with 114 additions and 65 deletions

View File

@ -39,7 +39,7 @@
*
* <p>All patterns are compatible with
* SimpleDateFormat (except time zones and some year patterns - see below).</p>
*
*
* <p>Since 3.2, FastDateFormat supports parsing as well as printing.</p>
*
* <p>Java 1.4 introduced a new pattern letter, {@code 'Z'}, to represent
@ -94,7 +94,7 @@ protected FastDateFormat createInstance(final String pattern, final TimeZone tim
private final FastDatePrinter printer;
private final FastDateParser parser;
//-----------------------------------------------------------------------
/**
* <p>Gets a formatter instance using the default pattern in the
@ -210,7 +210,7 @@ public static FastDateFormat getDateInstance(final int style, final Locale local
public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) {
return cache.getDateInstance(style, timeZone, null);
}
/**
* <p>Gets a date formatter instance using the specified style, time
* zone and locale.</p>
@ -366,8 +366,23 @@ public static FastDateFormat getDateTimeInstance(
* @throws NullPointerException if pattern, timeZone, or locale is null.
*/
protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) {
this(pattern, timeZone, locale, null);
}
// Constructor
//-----------------------------------------------------------------------
/**
* <p>Constructs a new FastDateFormat.</p>
*
* @param pattern {@link java.text.SimpleDateFormat} compatible pattern
* @param timeZone non-null time zone to use
* @param locale non-null locale to use
* @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years
* @throws NullPointerException if pattern, timeZone, or locale is null.
*/
protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
printer= new FastDatePrinter(pattern, timeZone, locale);
parser= new FastDateParser(pattern, timeZone, locale);
parser= new FastDateParser(pattern, timeZone, locale, centuryStart);
}
// Format methods
@ -463,7 +478,7 @@ public StringBuffer format(final Calendar calendar, final StringBuffer buf) {
// Parsing
//-----------------------------------------------------------------------
/* (non-Javadoc)
* @see DateParser#parse(java.lang.String)
*/

View File

@ -71,7 +71,7 @@ public class FastDateParser implements DateParser, Serializable {
*
* @see java.io.Serializable
*/
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 2L;
static final Locale JAPANESE_IMPERIAL = new Locale("ja","JP","JP");
@ -79,11 +79,12 @@ public class FastDateParser implements DateParser, Serializable {
private final String pattern;
private final TimeZone timeZone;
private final Locale locale;
private final int century;
private final int startYear;
// derived fields
private transient Pattern parsePattern;
private transient Strategy[] strategies;
private transient int thisYear;
// dynamic fields to communicate with Strategy
private transient String currentFormatField;
@ -96,21 +97,38 @@ public class FastDateParser implements DateParser, Serializable {
* pattern
* @param timeZone non-null time zone to use
* @param locale non-null locale
* @param centuryStart The start of the century for 2 digit year parsing
*/
protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) {
protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, Date centuryStart) {
this.pattern = pattern;
this.timeZone = timeZone;
this.locale = locale;
init();
final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
int centuryStartYear;
if(centuryStart!=null) {
definingCalendar.setTime(centuryStart);
centuryStartYear= definingCalendar.get(Calendar.YEAR);
}
else if(locale.equals(JAPANESE_IMPERIAL)) {
centuryStartYear= 0;
}
else {
// from 80 years ago to 20 years from now
definingCalendar.setTime(new Date());
centuryStartYear= definingCalendar.get(Calendar.YEAR)-80;
}
century= centuryStartYear / 100 * 100;
startYear= centuryStartYear - century;
init(definingCalendar);
}
/**
/**
* Initialize derived fields from defining fields.
* This is called from constructor and from readObject (de-serialization)
*/
private void init() {
final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
thisYear= definingCalendar.get(Calendar.YEAR);
private void init(Calendar definingCalendar) {
final StringBuilder regex= new StringBuilder();
final List<Strategy> collector = new ArrayList<Strategy>();
@ -176,7 +194,7 @@ public Locale getLocale() {
/**
* Returns the generated pattern (for testing purposes).
*
*
* @return the generated pattern
*/
Pattern getParsePattern() {
@ -234,7 +252,9 @@ public String toString() {
*/
private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
init();
final Calendar definingCalendar = Calendar.getInstance(timeZone, locale);
init(definingCalendar);
}
/* (non-Javadoc)
@ -319,11 +339,11 @@ private static StringBuilder escapeRegex(final StringBuilder regex, final String
case '\\':
if(++i==value.length()) {
break;
}
}
/*
* If we have found \E, we replace it with \E\\E\Q, i.e. we stop the quoting,
* quote the \ in \E, then restart the quoting.
*
*
* Otherwise we just output the two characters.
* In each case the initial \ needs to be output and the final char is done at the end
*/
@ -354,16 +374,13 @@ private static Map<String, Integer> getDisplayNames(final int field, final Calen
}
/**
* Adjust dates to be within 80 years before and 20 years after instantiation
* Adjust dates to be within appropriate century
* @param twoDigitYear The year to adjust
* @return A value within -80 and +20 years from instantiation of this instance
* @return A value between centuryStart(inclusive) to centuryStart+100(exclusive)
*/
int adjustYear(final int twoDigitYear) {
final int trial= twoDigitYear + thisYear - thisYear%100;
if(trial < thisYear+20) {
return trial;
}
return trial-100;
private int adjustYear(final int twoDigitYear) {
int trial= century + twoDigitYear;
return twoDigitYear>=startYear ?trial :trial+100;
}
/**
@ -389,7 +406,7 @@ private static abstract class Strategy {
/**
* Is this field a number?
* The default implementation returns false.
*
*
* @return true, if field is a number
*/
boolean isNumber() {
@ -397,15 +414,15 @@ boolean isNumber() {
}
/**
* Set the Calendar with the parsed field.
*
*
* The default implementation does nothing.
*
*
* @param parser The parser calling this strategy
* @param cal The <code>Calendar</code> to set
* @param value The parsed field to translate and set in cal
*/
void setCalendar(final FastDateParser parser, final Calendar cal, final String value) {
}
/**
* Generate a <code>Pattern</code> regular expression to the <code>StringBuilder</code>

View File

@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
@ -30,9 +31,8 @@
import java.util.Map;
import java.util.TimeZone;
import org.junit.Assert;
import org.apache.commons.lang3.SerializationUtils;
import org.junit.Assert;
import org.junit.Test;
/**
@ -42,8 +42,8 @@
* @since 3.2
*/
public class FastDateParserTest {
private static final String SHORT_FORMAT_NOERA = "y/M/d/h/a/m/E/Z";
private static final String LONG_FORMAT_NOERA = "yyyy/MMMM/dddd/hhhh/mmmm/aaaa/EEEE/ZZZZ";
private static final String SHORT_FORMAT_NOERA = "y/M/d/h/a/m/s/E/Z";
private static final String LONG_FORMAT_NOERA = "yyyy/MMMM/dddd/hhhh/mmmm/ss/aaaa/EEEE/ZZZZ";
private static final String SHORT_FORMAT = "G/" + SHORT_FORMAT_NOERA;
private static final String LONG_FORMAT = "GGGG/" + LONG_FORMAT_NOERA;
@ -79,7 +79,7 @@ private DateParser getInstance(final String format, final TimeZone timeZone) {
* Override this method in derived tests to change the construction of instances
*/
protected DateParser getInstance(final String format, final TimeZone timeZone, final Locale locale) {
return new FastDateParser(format, timeZone, locale);
return new FastDateParser(format, timeZone, locale, null);
}
@Test
@ -188,41 +188,58 @@ public void testAmPm() throws ParseException {
assertEquals(cal.getTime(), H.parse("2010-08-01 12:33:20"));
}
private Calendar getEraStart(int year, TimeZone zone, Locale locale) {
Calendar cal = Calendar.getInstance(zone, locale);
cal.clear();
// http://docs.oracle.com/javase/6/docs/technotes/guides/intl/calendar.doc.html
if (locale.equals(FastDateParser.JAPANESE_IMPERIAL)) {
if(year < 1868) {
cal.set(Calendar.ERA, 0);
cal.set(Calendar.YEAR, 1868-year);
}
}
else {
if (year < 0) {
cal.set(Calendar.ERA, GregorianCalendar.BC);
year= -year;
}
cal.set(Calendar.YEAR, year/100 * 100);
}
return cal;
}
private void validateSdfFormatFdpParseEquality(String format, Locale locale, TimeZone tz, DateParser fdp, Date in, int year, Date cs) throws ParseException {
final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
if (format.equals(SHORT_FORMAT)) {
sdf.set2DigitYearStart( cs );
}
final String fmt = sdf.format(in);
try {
final Date out = fdp.parse(fmt);
assertEquals(locale.toString()+" "+in+" "+ format+ " "+tz.getID(), in, out);
} catch (final ParseException pe) {
System.out.println(fmt+" "+locale.toString()+" "+year+" "+ format+ " "+tz.getID());
throw pe;
}
}
@Test
// Check that all Locales can parse the formats we use
public void testParses() throws Exception {
for(final Locale locale : Locale.getAvailableLocales()) {
for(final TimeZone tz : new TimeZone[]{NEW_YORK, GMT}) {
final Calendar cal = Calendar.getInstance(tz);
for(final int year : new int[]{2003, 1940, 1868, 1867, 0, -1940}) {
// http://docs.oracle.com/javase/6/docs/technotes/guides/intl/calendar.doc.html
if (year < 1868 && locale.equals(FastDateParser.JAPANESE_IMPERIAL)) {
continue; // Japanese imperial calendar does not support eras before 1868
}
cal.clear();
if (year < 0) {
cal.set(-year, 1, 10);
cal.set(Calendar.ERA, GregorianCalendar.BC);
} else {
cal.set(year, 1, 10);
}
final Date in = cal.getTime();
for(final String format : new String[]{LONG_FORMAT, SHORT_FORMAT}) {
final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
if (format.equals(SHORT_FORMAT)) {
if (year < 1930) {
sdf.set2DigitYearStart(cal.getTime());
}
}
final String fmt = sdf.format(in);
try {
final Date out = sdf.parse(fmt);
for(final String format : new String[]{LONG_FORMAT, SHORT_FORMAT}) {
for(final Locale locale : Locale.getAvailableLocales()) {
for(final TimeZone tz : new TimeZone[]{NEW_YORK, REYKJAVIK, GMT}) {
for(final int year : new int[]{2003, 1940, 1868, 1867, 1, -1, -1940}) {
Calendar cal= getEraStart(year, tz, locale);
Date centuryStart= cal.getTime();
assertEquals(locale.toString()+" "+year+" "+ format+ " "+tz.getID(), in, out);
} catch (final ParseException pe) {
System.out.println(fmt+" "+locale.toString()+" "+year+" "+ format+ " "+tz.getID());
throw pe;
}
cal.set(Calendar.MONTH, 1);
cal.set(Calendar.DAY_OF_MONTH, 10);
Date in= cal.getTime();
final FastDateParser fdp= new FastDateParser(format, tz, locale, centuryStart);
validateSdfFormatFdpParseEquality(format, locale, tz, fdp, in, year, centuryStart);
}
}
}