Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,26 @@ public static Date parse(String date, ParsePosition pos) throws ParseException {
if (!hasT && (date.length() <= offset)) {
Calendar calendar = new GregorianCalendar(year, month - 1, day);
calendar.setLenient(false);

pos.setIndex(offset);
return calendar.getTime();
try {
pos.setIndex(offset);
return calendar.getTime();
} catch (IllegalArgumentException e) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me a bit uneasy. I'm sure there's more than one way we could get this exception.

Could we just construct a GregorianCalendar instance that specifies noon instead of midnight? Then we should be able to avoid the issue.

I also think ultimately we should rewrite this method so it uses java.time APIs, but that's a bigger project, and might have implications for Android (with old target SDKs and no desugaring).

// The calendar date can be valid while local midnight does not exist in the default
// time zone, for example '1966-11-01' in America/Sao_Paulo where clocks were shifted
// forward from 0:00 to 1:00. In that case resolve the date like java.time's
// LocalDate.atStartOfDay(ZoneId) by using the first existing instant of that day.
// Truly invalid dates (such as '2021-02-30') also fail above, but lenient resolution
// would shift them to a different day, so they are re-thrown below.
Calendar lenientCalendar = new GregorianCalendar(year, month - 1, day);
Date resolved = lenientCalendar.getTime();
if (lenientCalendar.get(Calendar.YEAR) != year
|| lenientCalendar.get(Calendar.MONTH) != month - 1
|| lenientCalendar.get(Calendar.DAY_OF_MONTH) != day) {
throw e;
}
pos.setIndex(offset);
return resolved;
}
}

if (hasT) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,50 @@ private static GregorianCalendar createUtcCalendar() {
return calendar;
}

@Test
public void testParseDateOnlyDSTGap() throws ParseException {
TimeZone defaultTimeZone = TimeZone.getDefault();
try {
// 1966-11-01 00:00 does not exist in America/Sao_Paulo because clocks were shifted

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example makes it look as if this is a problem that almost nobody will have. Googling suggests that there are present-day timezones that switch to and from DST at midnight. I believe you can use America/Santiago and 2026-09-06 for a more realistic test case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that almost nobody will have

It is the exact reproducer from #2539 though; and based on that issue it is parsed as date of birth, so not such an unreasonable / uncommon case?

(Not saying though that a reproducer with a more recent date wouldn't be useful.)

// forward from 0:00 to 1:00 at midnight; parsing must not fail for this valid date
TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
Date date = ISO8601Utils.parse("1966-11-01", new ParsePosition(0));

GregorianCalendar calendar =
new GregorianCalendar(TimeZone.getTimeZone("America/Sao_Paulo"), Locale.US);
// Calendar was created with current time, must clear it
calendar.clear();
calendar.setTime(date);
assertThat(calendar.get(Calendar.YEAR)).isEqualTo(1966);
assertThat(calendar.get(Calendar.MONTH)).isEqualTo(Calendar.NOVEMBER);
assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(1);
// The resolved hour depends on the DST rules of the JDK's bundled timezone data
// (midnight itself, or the first hour after the 0:00 -> 1:00 transition), so only
// assert that the date fields are preserved while the hour stays within the day
assertThat(calendar.get(Calendar.HOUR_OF_DAY)).isAnyOf(0, 1);
} finally {
TimeZone.setDefault(defaultTimeZone);
}
}

@Test
public void testParseInvalidDateOnlyStillFails() {
TimeZone defaultTimeZone = TimeZone.getDefault();
try {
// Strict parsing introduced for date-only values must keep rejecting dates which do
// not exist, even in time zones where midnight is skipped by a DST transition
TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
assertThrows(
ParseException.class, () -> ISO8601Utils.parse("2021-02-30", new ParsePosition(0)));
assertThrows(
ParseException.class, () -> ISO8601Utils.parse("2021-13-01", new ParsePosition(0)));
assertThrows(
ParseException.class, () -> ISO8601Utils.parse("1966-00-01", new ParsePosition(0)));
} finally {
TimeZone.setDefault(defaultTimeZone);
}
}

@Test
public void testDateFormatString() {
GregorianCalendar calendar = new GregorianCalendar(utcTimeZone(), Locale.US);
Expand Down