Skip to content

Fix date-only parsing failure when local midnight is skipped by DST - #3109

Open
zgj-ssslab wants to merge 4 commits into
google:mainfrom
zgj-ssslab:fix/date-parse-timezone-dst
Open

zgj-ssslab wants to merge 4 commits into
google:mainfrom
zgj-ssslab:fix/date-parse-timezone-dst

Conversation

@zgj-ssslab

Copy link
Copy Markdown

Description

Parsing a date-only string (e.g. "1966-11-01") fails with IllegalArgumentException: HOUR_OF_DAY: 0 -> 1 when the default time zone skips midnight on that date due to a DST transition (e.g. America/Sao_Paulo in 1966).

Root cause

For date-only input, ISO8601Utils.parse constructs a GregorianCalendar for local midnight in the default time zone with setLenient(false). When a DST transition shifts clocks from 0:00 to 1:00, that wall time does not exist and the non-lenient calendar rejects it — even though the calendar date itself is valid.

Fix

When the strict calendar fails, resolve leniently only if the date fields (year/month/day) are unchanged by the lenient resolution — i.e. the date exists but its midnight was skipped; the result is the first existing instant of that day, matching the semantics of java.time's LocalDate.atStartOfDay(ZoneId). Genuinely invalid dates (e.g. 2021-02-30) shift to a different day under lenient resolution and keep failing, preserving the strict behavior.

How I checked

  • Reproduced the failure with America/Sao_Paulo + 1966-11-01; verified all other time zones/dates unchanged
  • Added regression tests: DST-gap date parses to the first existing instant; 2021-02-30, 2021-13-01, 1966-00-01 still fail
  • Full module test suite passes (4635 tests)

Fixes #2539

Parsing a date-only string like '1966-11-01' with default time zone
America/Sao_Paulo threw an exception ('HOUR_OF_DAY: 0 -> 1') because
local midnight did not exist due to a DST transition at midnight.

Strict parsing now only fails for dates which do not exist in the
calendar (e.g. 2021-02-30); a valid date whose midnight is skipped by
DST is resolved to the first existing instant of that day, following
the semantics of java.time's LocalDate.atStartOfDay(ZoneId).

Fixes google#2539
@google-cla

google-cla Bot commented Sep 4, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

The exact resolved hour depends on the DST rules of the JDK's bundled
timezone data, which differs between JDK versions. Assert only that the
date fields are preserved and the resolved hour is within the day.
@Marcono1234

Copy link
Copy Markdown
Contributor

Could you please run mvn spotless:apply and commit the changes? Your PR currently changes the line endings of the modified files from LF (\n) to CRLF (\r\n); this is also the reason for the CI failure.

The two modified files had been committed with CRLF line endings, which
fails the spotless check. Re-applied via `mvn spotless:apply`; the only
change is 
 removal, no formatting or code changes.
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.)

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).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug Parse Date 1966-11-01

4 participants