LANG-1192: FastDateFormat support of the week-year component (uppercase 'Y')
This commit is contained in:
parent
39ed92fff7
commit
afc942c7b2
|
@ -22,6 +22,7 @@
|
|||
<body>
|
||||
|
||||
<release version="3.5" date="tba" description="tba">
|
||||
<action issue="LANG-1192" type="add" dev="chas" due-to="Dominik Stadler">FastDateFormat support of the week-year component (uppercase 'Y')</action>
|
||||
<action issue="LANG-1194" type="fix" dev="chas">Limit max heap memory for consistent Travis CI build</action>
|
||||
<action issue="LANG-1186" type="fix" dev="chas" due-to="NickManley">Fix NullPointerException in FastDateParser$TimeZoneStrategy</action>
|
||||
<action issue="LANG-1193" type="fix" dev="sebb" due-to="Qin Li">ordinalIndexOf("abc", "ab", 1) gives incorrect answer of -1 (correct answer should be 0); revert fix for LANG-1077</action>
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.commons.lang3.time;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
/**
|
||||
* Use reflection to access java 1.7 methods in Calendar. This allows compilation with 1.6 compiler.
|
||||
*/
|
||||
class CalendarReflection {
|
||||
|
||||
private static final Method IS_WEEK_DATE_SUPPORTED = getCalendarMethod("isWeekDateSupported");
|
||||
private static final Method GET_WEEK_YEAR = getCalendarMethod("getWeekYear");
|
||||
|
||||
private static Method getCalendarMethod(String methodName, Class<?>... argTypes) {
|
||||
try {
|
||||
Method m = Calendar.class.getMethod(methodName, argTypes);
|
||||
return m;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this calendar instance support week date?
|
||||
* @param calendar The calendar instance.
|
||||
* @return false, if runtime is less than java 1.7; otherwise, the result of calendar.isWeekDateSupported().
|
||||
*/
|
||||
static boolean isWeekDateSupported(Calendar calendar) {
|
||||
try {
|
||||
return IS_WEEK_DATE_SUPPORTED!=null && ((Boolean)IS_WEEK_DATE_SUPPORTED.invoke(calendar)).booleanValue();
|
||||
} catch (Exception e) {
|
||||
return ExceptionUtils.<Boolean>rethrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke getWeekYear() method of calendar instance.
|
||||
* <p>
|
||||
* If runtime is 1.7 or better and calendar instance support week year,
|
||||
* return the value from invocation of getWeekYear().
|
||||
* <p>
|
||||
* If runtime is less than 1.7, and calendar is an instance of
|
||||
* GregorianCalendar, return an approximation of the week year.
|
||||
* (Approximation is good for all years after the Julian to Gregorian
|
||||
* cutover.)
|
||||
* <p>
|
||||
* Otherwise, return the calendar instance year value.
|
||||
*
|
||||
* @param calendar The calendar instance.
|
||||
* @return the week year or year value.
|
||||
*/
|
||||
public static int getWeekYear(Calendar calendar) {
|
||||
try {
|
||||
if (isWeekDateSupported(calendar)) {
|
||||
return (Integer) GET_WEEK_YEAR.invoke(calendar);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ExceptionUtils.<Integer> rethrow(e);
|
||||
}
|
||||
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
if (IS_WEEK_DATE_SUPPORTED == null && calendar instanceof GregorianCalendar) {
|
||||
// not perfect, won't work before gregorian cutover
|
||||
// good enough for most business use.
|
||||
switch (calendar.get(Calendar.MONTH)) {
|
||||
case Calendar.JANUARY:
|
||||
if (calendar.get(Calendar.WEEK_OF_YEAR) >= 52) {
|
||||
--year;
|
||||
}
|
||||
break;
|
||||
case Calendar.DECEMBER:
|
||||
if (calendar.get(Calendar.WEEK_OF_YEAR) == 1) {
|
||||
++year;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return year;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ package org.apache.commons.lang3.time;
|
|||
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
@ -53,6 +54,21 @@ public interface DateParser {
|
|||
*/
|
||||
Date parse(String source, ParsePosition pos);
|
||||
|
||||
/**
|
||||
* Parse a formatted date string according to the format. Updates the Calendar with parsed fields.
|
||||
* Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed.
|
||||
* Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to
|
||||
* the offset of the source text which does not match the supplied format.
|
||||
*
|
||||
* @param source The text to parse.
|
||||
* @param pos On input, the position in the source to start parsing, on output, updated position.
|
||||
* @param calendar The calendar into which to set parsed fields.
|
||||
* @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated)
|
||||
* @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is
|
||||
* out of range.
|
||||
*/
|
||||
boolean parse(String source, ParsePosition pos, Calendar calendar);
|
||||
|
||||
// Accessors
|
||||
//-----------------------------------------------------------------------
|
||||
/**
|
||||
|
|
|
@ -550,7 +550,16 @@ public class FastDateFormat extends Format implements DateParser, DatePrinter {
|
|||
*/
|
||||
@Override
|
||||
public Date parse(final String source, final ParsePosition pos) {
|
||||
return parser.parse(source, pos);
|
||||
return parser.parse(source, pos);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.apache.commons.lang3.time.DateParser#parse(java.lang.String, java.text.ParsePosition, java.util.Calendar)
|
||||
*/
|
||||
@Override
|
||||
public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
|
||||
return parser.parse(source, pos, calendar);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
|
|
@ -170,7 +170,7 @@ public class FastDateParser implements DateParser, Serializable {
|
|||
//-----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Struct to hold strategy and filed width
|
||||
* Struct to hold strategy and field width
|
||||
*/
|
||||
private static class StrategyAndWidth {
|
||||
final Strategy strategy;
|
||||
|
@ -401,12 +401,12 @@ public class FastDateParser implements DateParser, Serializable {
|
|||
|
||||
return parse(source, pos, cal) ?cal.getTime() :null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a formatted date string according to the format. Updates the Calendar with parsed fields.
|
||||
* Parse a formatted date string according to the format. Updates the Calendar with parsed fields.
|
||||
* Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed.
|
||||
* Not all source text needs to be consumed. Upon parse failure, ParsePosition error index is updated to
|
||||
* the offset of the source text which does not match the supplied format.
|
||||
* the offset of the source text which does not match the supplied format.
|
||||
*
|
||||
* @param source The text to parse.
|
||||
* @param pos On input, the position in the source to start parsing, on output, updated position.
|
||||
|
@ -415,17 +415,18 @@ public class FastDateParser implements DateParser, Serializable {
|
|||
* @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is
|
||||
* out of range.
|
||||
*/
|
||||
public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
|
||||
ListIterator<StrategyAndWidth> lt = patterns.listIterator();
|
||||
while(lt.hasNext()) {
|
||||
StrategyAndWidth pattern = lt.next();
|
||||
int maxWidth = pattern.getMaxWidth(lt);
|
||||
if(!pattern.strategy.parse(this, calendar, source, pos, maxWidth)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) {
|
||||
ListIterator<StrategyAndWidth> lt = patterns.listIterator();
|
||||
while(lt.hasNext()) {
|
||||
StrategyAndWidth pattern = lt.next();
|
||||
int maxWidth = pattern.getMaxWidth(lt);
|
||||
if(!pattern.strategy.parse(this, calendar, source, pos, maxWidth)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Support for strategies
|
||||
//-----------------------------------------------------------------------
|
||||
|
@ -606,6 +607,7 @@ public class FastDateParser implements DateParser, Serializable {
|
|||
case 'w':
|
||||
return WEEK_OF_YEAR_STRATEGY;
|
||||
case 'y':
|
||||
case 'Y':
|
||||
return width>2 ?LITERAL_YEAR_STRATEGY :ABBREVIATED_YEAR_STRATEGY;
|
||||
case 'X':
|
||||
return ISO8601TimeZoneStrategy.getStrategy(width);
|
||||
|
|
|
@ -25,7 +25,6 @@ import java.text.FieldPosition;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
@ -211,11 +210,15 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
rule = new TextField(Calendar.ERA, ERAs);
|
||||
break;
|
||||
case 'y': // year (number)
|
||||
case 'Y': // week year
|
||||
if (tokenLen == 2) {
|
||||
rule = TwoDigitYearField.INSTANCE;
|
||||
} else {
|
||||
rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen);
|
||||
}
|
||||
if (c == 'Y') {
|
||||
rule = new WeekYear((NumberRule) rule);
|
||||
}
|
||||
break;
|
||||
case 'M': // month in year (text and number)
|
||||
if (tokenLen >= 4) {
|
||||
|
@ -438,7 +441,7 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
*/
|
||||
@Override
|
||||
public String format(final long millis) {
|
||||
final Calendar c = newCalendar(); // hard code GregorianCalendar
|
||||
final Calendar c = newCalendar();
|
||||
c.setTimeInMillis(millis);
|
||||
return applyRulesToString(c);
|
||||
}
|
||||
|
@ -453,12 +456,11 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creation method for ne calender instances.
|
||||
* Creation method for new calender instances.
|
||||
* @return a new Calendar instance.
|
||||
*/
|
||||
private GregorianCalendar newCalendar() {
|
||||
// hard code GregorianCalendar
|
||||
return new GregorianCalendar(mTimeZone, mLocale);
|
||||
private Calendar newCalendar() {
|
||||
return Calendar.getInstance(mTimeZone, mLocale);
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
|
@ -466,7 +468,7 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
*/
|
||||
@Override
|
||||
public String format(final Date date) {
|
||||
final Calendar c = newCalendar(); // hard code GregorianCalendar
|
||||
final Calendar c = newCalendar();
|
||||
c.setTime(date);
|
||||
return applyRulesToString(c);
|
||||
}
|
||||
|
@ -492,7 +494,7 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
*/
|
||||
@Override
|
||||
public StringBuffer format(final Date date, final StringBuffer buf) {
|
||||
final Calendar c = newCalendar(); // hard code GregorianCalendar
|
||||
final Calendar c = newCalendar();
|
||||
c.setTime(date);
|
||||
return applyRules(c, buf);
|
||||
}
|
||||
|
@ -519,7 +521,7 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
*/
|
||||
@Override
|
||||
public <B extends Appendable> B format(final Date date, final B buf) {
|
||||
final Calendar c = newCalendar(); // hard code GregorianCalendar
|
||||
final Calendar c = newCalendar();
|
||||
c.setTime(date);
|
||||
return applyRules(c, buf);
|
||||
}
|
||||
|
@ -528,9 +530,13 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
* @see org.apache.commons.lang3.time.DatePrinter#format(java.util.Calendar, java.lang.Appendable)
|
||||
*/
|
||||
@Override
|
||||
public <B extends Appendable> B format(final Calendar calendar, final B buf) {
|
||||
public <B extends Appendable> B format(Calendar calendar, final B buf) {
|
||||
// do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored
|
||||
return format(calendar.getTime(), buf);
|
||||
if(!calendar.getTimeZone().equals(mTimeZone)) {
|
||||
calendar = (Calendar)calendar.clone();
|
||||
calendar.setTimeZone(mTimeZone);
|
||||
}
|
||||
return applyRules(calendar, buf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1202,6 +1208,32 @@ public class FastDatePrinter implements DatePrinter, Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Inner class to output the numeric day in week.</p>
|
||||
*/
|
||||
private static class WeekYear implements NumberRule {
|
||||
private final NumberRule mRule;
|
||||
|
||||
WeekYear(final NumberRule rule) {
|
||||
mRule = rule;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int estimateLength() {
|
||||
return mRule.estimateLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException {
|
||||
mRule.appendTo(buffer, CalendarReflection.getWeekYear(calendar));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendTo(final Appendable buffer, final int value) throws IOException {
|
||||
mRule.appendTo(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
private static final ConcurrentMap<TimeZoneDisplayKey, String> cTimeZoneDisplayCache =
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.apache.commons.lang3.time;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class WeekYearTest {
|
||||
|
||||
@Parameters(name = "{index}: {3}")
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays
|
||||
.asList(new Object[][] {
|
||||
{ 2005, Calendar.JANUARY, 1, "2004-W53-6" },
|
||||
{ 2005, Calendar.JANUARY, 2, "2004-W53-7" },
|
||||
{ 2005, Calendar.DECEMBER, 31, "2005-W52-6" },
|
||||
{ 2007, Calendar.JANUARY, 1, "2007-W01-1" },
|
||||
{ 2007, Calendar.DECEMBER, 30, "2007-W52-7" },
|
||||
{ 2007, Calendar.DECEMBER, 31, "2008-W01-1" },
|
||||
{ 2008, Calendar.JANUARY, 1, "2008-W01-2" },
|
||||
{ 2008, Calendar.DECEMBER, 28, "2008-W52-7" },
|
||||
{ 2008, Calendar.DECEMBER, 29, "2009-W01-1" },
|
||||
{ 2008, Calendar.DECEMBER, 30, "2009-W01-2" },
|
||||
{ 2008, Calendar.DECEMBER, 31, "2009-W01-3" },
|
||||
{ 2009, Calendar.JANUARY, 1, "2009-W01-4" },
|
||||
{ 2009, Calendar.DECEMBER, 31, "2009-W53-4" },
|
||||
{ 2010, Calendar.JANUARY, 1, "2009-W53-5" },
|
||||
{ 2010, Calendar.JANUARY, 2, "2009-W53-6" },
|
||||
{ 2010, Calendar.JANUARY, 3, "2009-W53-7" }
|
||||
});
|
||||
}
|
||||
|
||||
final Calendar vulgar;
|
||||
final String isoForm;
|
||||
|
||||
public WeekYearTest(int year, int month, int day, String isoForm) {
|
||||
vulgar = new GregorianCalendar(year, month, day);
|
||||
this.isoForm = isoForm;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParser() throws ParseException {
|
||||
final DateParser parser = new FastDateParser("YYYY-'W'ww-u", TimeZone.getDefault(), Locale.getDefault());
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setMinimalDaysInFirstWeek(4);
|
||||
cal.setFirstDayOfWeek(Calendar.MONDAY);
|
||||
cal.clear();
|
||||
|
||||
parser.parse(isoForm, new ParsePosition(0), cal);
|
||||
Assert.assertEquals(vulgar.getTime(), cal.getTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrinter() {
|
||||
final FastDatePrinter printer = new FastDatePrinter("YYYY-'W'ww-u", TimeZone.getDefault(), Locale.getDefault());
|
||||
|
||||
vulgar.setMinimalDaysInFirstWeek(4);
|
||||
vulgar.setFirstDayOfWeek(Calendar.MONDAY);
|
||||
|
||||
Assert.assertEquals(isoForm, printer.format(vulgar));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue