-
-
Save not-napoleon/7b3adbea80f3fafe16ebae001586a763 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.junit.Test; | |
| import java.time.ZoneId; | |
| import java.time.ZoneOffset; | |
| import java.time.ZonedDateTime; | |
| import java.time.format.DateTimeFormatter; | |
| import java.time.format.DateTimeFormatterBuilder; | |
| import java.time.format.SignStyle; | |
| import java.time.temporal.ChronoField; | |
| import java.time.temporal.TemporalAccessor; | |
| import java.util.Locale; | |
| import static java.time.temporal.ChronoField.DAY_OF_MONTH; | |
| import static java.time.temporal.ChronoField.HOUR_OF_DAY; | |
| import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; | |
| import static java.time.temporal.ChronoField.MONTH_OF_YEAR; | |
| import static java.time.temporal.ChronoField.NANO_OF_SECOND; | |
| import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; | |
| import static org.junit.Assert.assertEquals; | |
| public class DateParseTests { | |
| /** | |
| * This test shows the issue. We have a string in unambiguous seconds since epoch. We parse this | |
| * and it resolves to an ambiguous time in America/New_York, which then gets represented as a different | |
| * seconds since epoch value. | |
| * | |
| * The formatting step isn't necessary as part of the test, the error happens in the parse. Based on | |
| * tracing this in the debugger, it seems like the problem occurs in java.time.format.Parsed#resolve() | |
| * which correctly parses an unambiguous seconds value, which it then converts to a date + time + timezone | |
| * value, which is ambiguous. Then a later step converts that back to a seconds since epoch, but picks | |
| * the wrong one | |
| */ | |
| @Test | |
| public void testEpochSecondsWithDSTEnd() { | |
| final DateTimeFormatter epochSecondFormatter = new DateTimeFormatterBuilder() | |
| .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NORMAL) | |
| .toFormatter(Locale.ROOT) | |
| .withZone(ZoneId.of("America/New_York")); | |
| // This is the transition point from DST to Standard time for that year in America/New_York tz | |
| ZonedDateTime sixAm = ZonedDateTime.of(2020, 11, 1, 6, 0, 0, 0, ZoneOffset.UTC); | |
| assertEquals(1604210400000L, sixAm.toInstant().toEpochMilli()); | |
| assertEquals("1604210400", epochSecondFormatter.format(sixAm)); | |
| TemporalAccessor actual = epochSecondFormatter.parse(epochSecondFormatter.format(sixAm)); | |
| /* | |
| java.lang.AssertionError: | |
| Expected :1604210400 | |
| Actual :1604206800 | |
| */ | |
| assertEquals(sixAm.toEpochSecond(), actual.getLong(ChronoField.INSTANT_SECONDS)); | |
| } | |
| /** | |
| * This shows that it parses fine when there isn't a DST to Standard transition | |
| */ | |
| @Test | |
| public void testEpochSecondsWithTimeZone() { | |
| final DateTimeFormatter epochSecondFormatter = new DateTimeFormatterBuilder() | |
| .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NORMAL) | |
| .toFormatter(Locale.ROOT) | |
| .withZone(ZoneId.of("America/New_York")); | |
| ZonedDateTime sixAm = ZonedDateTime.of(2020, 10, 1, 6, 0, 0, 0, ZoneOffset.UTC); | |
| TemporalAccessor actual = epochSecondFormatter.parse(epochSecondFormatter.format(sixAm)); | |
| // Passes | |
| assertEquals(sixAm.toEpochSecond(), actual.getLong(ChronoField.INSTANT_SECONDS)); | |
| } | |
| /** | |
| * This test does the same thing, but with a more elaborate format. We still serialize an | |
| * unambiguous time, parse it, and spit out a new value. But in this case, we get the correct | |
| * result. | |
| */ | |
| @Test | |
| public void testIsoFormatDstEnd() { | |
| final DateTimeFormatter isoFormatter = new DateTimeFormatterBuilder() | |
| .appendValue(ChronoField.YEAR, 4, 4, SignStyle.EXCEEDS_PAD) | |
| .optionalStart() | |
| .appendLiteral("-") | |
| .appendValue(MONTH_OF_YEAR, 2, 2, SignStyle.NOT_NEGATIVE) | |
| .optionalStart() | |
| .appendLiteral('-') | |
| .appendValue(DAY_OF_MONTH, 2, 2, SignStyle.NOT_NEGATIVE) | |
| .optionalEnd() | |
| .optionalEnd() | |
| .appendLiteral('T') | |
| .optionalStart() | |
| .appendValue(HOUR_OF_DAY, 2, 2, SignStyle.NOT_NEGATIVE) | |
| .optionalStart() | |
| .appendLiteral(':') | |
| .appendValue(MINUTE_OF_HOUR, 2, 2, SignStyle.NOT_NEGATIVE) | |
| .optionalStart() | |
| .appendLiteral(':') | |
| .appendValue(SECOND_OF_MINUTE, 2, 2, SignStyle.NOT_NEGATIVE) | |
| .optionalStart() | |
| .appendFraction(NANO_OF_SECOND, 1, 9, true) | |
| .optionalEnd() | |
| .optionalStart() | |
| .appendLiteral(",") | |
| .appendFraction(NANO_OF_SECOND, 1, 9, false) | |
| .optionalEnd() | |
| .optionalEnd() | |
| .optionalEnd() | |
| .optionalEnd() | |
| .optionalStart() | |
| .appendZoneOrOffsetId() | |
| .optionalEnd() | |
| .optionalStart() | |
| .appendOffset("+HHmm", "Z") | |
| .optionalEnd() | |
| .toFormatter(Locale.ROOT) | |
| .withZone(ZoneId.of("America/New_York")); | |
| // This is the transition point from DST to Standard time for that year in America/New_York tz | |
| ZonedDateTime sixAm = ZonedDateTime.of(2020, 11, 1, 6, 0, 0, 0, ZoneOffset.UTC); | |
| assertEquals(1604210400000L, sixAm.toInstant().toEpochMilli()); | |
| assertEquals("2020-11-01T01:00:00.0,0America/New_York-05", isoFormatter.format(sixAm)); | |
| TemporalAccessor actual = isoFormatter.parse(isoFormatter.format(sixAm)); | |
| assertEquals(sixAm.toEpochSecond(), actual.getLong(ChronoField.INSTANT_SECONDS)); | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
We submitted this as a bug, and the JDK team fixed it: https://bugs.openjdk.java.net/browse/JDK-8272473