jvir
jvir

Reputation: 31

Daylight Saving Days problem calculating the dates

I need to calculate Dates from one date I read from a Excel, the problem is that for example, if I read 31/10/2021 02:00:00 and I add 1 hour, it should give me back again the same hour, because there is time change so at 03:00:00 would be again 02:00:00, the problem is that when I read from excel 31/10/2021 02:00:00 the date is created with CET, so if I add the hour it would show 03:00 instead of 02:00:00 that is what I need.

I´m using java Calendar with the date I read from excel and then adding 1 hour to calendar. Any idea how should I do so its repeats again 02:00 because of the daylight saving?

Upvotes: 3

Views: 885

Answers (1)

Basil Bourque
Basil Bourque

Reputation: 339482

tl;dr

Asking for the beginning or ending of the 2-3 AM "Fall Back" hour of Daylight Saving Time (DST), in java.time you get:

  • The earlier of two actual 2 AM moments.
  • The later of two potential 3 AM moments.

So two hours elapse between the requested 2 AM and the requested 3 AM.

  • To get the first 2 AM, call LocalDateTime.atZone while asking for 2 AM.
  • To get an hour later, the second 2 AM, follow that LocalDateTime.atZone with a call to ZonedDateTime.withLaterOffsetAtOverlap().
  • To get yet another hour later, 3 AM, call LocalDateTime.atZone while asking for 3 AM.

Details

I´m using java object Calendar

Don’t.

That terrible class was supplanted years ago by the modern java.time classes defined in JSR 310. Specifically replaced by ZonedDateTime.

if I read 31/10/2021 02:00:00

Parse that date with time-of-day as a LocalDateTime object.

String input = "31/10/2021 02:00:00";
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd/MM/uuuu HH:mm:ss" );
LocalDateTime ldt = LocalDateTime.parse( input , f );

ldt.toString() = 2021-10-31T02:00

You need to be very aware that this date with time does not represent a moment. Without the context of a time zone or offset-from-UTC, we do not know if this is 2 AM in Tokyo, Toulouse, or Toledo.

the date is created with CET

CET is not a real time zone. Real time zones have a name in format of Continent/Region such as Africa/Casablanca or Europe/Paris.

Let's use Europe/Madrid as an example of your intended time zone.

ZoneId z = ZoneId.of( "Europe/Madrid" ) ;

Apply that zone to our LocalDateTime object to determine a moment. Apply a ZoneId object to get a ZonedDateTime object.

ZonedDateTime zdt = ldt.atZone( z ) ;

The trick here is that as mentioned in the Question, 2 AM happened twice on that date in that zone. After the first 2 AM happened, time passed through the hour. Upon reaching 3 AM, per the dictates of politicians, the clock rolled back to 2 AM again. And the clock proceeded to roll through the entire hour of 2 AM to 3 AM a second time.

So when asking for 2 AM on that date in that zone, which 2 AM do you mean? There is no obvious answer. Either the first 2 AM or the 2 AM, either is legitimate, yet the two represent two different moments an hour apart on the timeline of the Universe.

So what does LocalDateTime#atZone do to resolve this ambiguity? The Javadoc says: (my emphasis)

In most cases, there is only one valid offset for a local date-time. In the case of an overlap, where clocks are set back, there are two valid offsets. This method uses the earlier offset typically corresponding to "summer".

So the first of the two 2 AM moments is used when adjusting from a date-with-time to a date-with-time-with-zone.

Let's verify this behavior. Before we get dizzy from all this time zone business, we should get grounded by getting back to that timeline of the Universe. For that timeline, we use Instant class to represent a moment as seen with an offset of zero hours-minutes-seconds from UTC. Think of UTC time as the One True Time™. Programmers and Sysadmins really should learn to think in UTC rather their own parochial time zone, when on the job.

System.out.println( "zdt.toInstant().toString() = " + zdt.toInstant().toString() );

Our result in UTC time is the first moment of the new day on the 31st.

zdt.toInstant().toString() = 2021-10-31T00:00:00Z

This result makes sense as the Daylight Saving Time (DST) ("Summer Time") in Spain of 2021 was two hours ahead. So the earlier of our two Europe/Spain 2 AM moments happened while the Spanish clocks were set 2 hours ahead of UTC. FYI, here is a reference page of offset changes for Europe/Spain.

Let's ask for 3 AM, the time when the clocks jumped backward, "Fall Back" in English as we say. Upon reaching 3 AM the first time, the clocks jump back in time an hour, to repeat 2 AM all over again. Let's see if we get that behavior with java.time.

ZonedDateTime threeAmMadrid = ZonedDateTime.of( 2021 , 10 , 31 , 3 , 0 , 0 , 0 , z );
System.out.println( "threeAmMadrid = " + threeAmMadrid );
System.out.println( "threeAmMadrid.toInstant().toString() = " + threeAmMadrid.toInstant().toString() );

threeAmMadrid = 2021-10-31T03:00+01:00[Europe/Madrid]

threeAmMadrid.toInstant().toString() = 2021-10-31T02:00:00Z

We do not see the clock jump back. This behavior makes sense. If java.time were to jump back to 2 AM, we would never be able to represent the 2nd occurrence of 3 AM Madrid time. So the behavior here is to represent that 2nd occurrence of 3 AM. We can verify this by examining the time on our Instant object. Now we see 02:00:00, which is two hours later than our first Instant. So this adds up, asking for 2 AM gets us the first of two actual 2 AM moments, while asking for 3 AM gets us the second of two potential 3 AM moments, with two hours elapsing because the 2-3 hour repeated.


By the way, perhaps you want to access the second of the two actual 2 AM moments. The Javadoc for LocalDateTime#atZone explains the solution:

To obtain the later offset during an overlap, call ZonedDateTime.withLaterOffsetAtOverlap() on the result of this method.

Some code.

LocalDateTime ldt = LocalDateTime.of( 2021 , 10 , 31 , 2 , 0 , 0 , 0 );
ZoneId z = ZoneId.of( "Europe/Madrid" );
ZonedDateTime first2AM = ldt.atZone( z );
ZonedDateTime second2AM = first2AM.withLaterOffsetAtOverlap();

System.out.println( "first2AM = " + first2AM );
System.out.println( "first2AM.toInstant().toString() = " + first2AM.toInstant().toString() );

System.out.println( "second2AM = " + second2AM );
System.out.println( "second2AM.toInstant().toString() = " + second2AM.toInstant().toString() );

Notice how in the zoned values, both show 2 AM but the offset differs, two hours ahead of UTC versus one hour ahead.

And notice how in the UTC values, the time-of-day differs by an hour.

first2AM = 2021-10-31T02:00+02:00[Europe/Madrid]

first2AM.toInstant().toString() = 2021-10-31T00:00:00Z

second2AM = 2021-10-31T02:00+01:00[Europe/Madrid]

second2AM.toInstant().toString() = 2021-10-31T01:00:00Z

Upvotes: 3

Related Questions