Reputation: 31
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
Reputation: 339482
Asking for the beginning or ending of the 2-3 AM "Fall Back" hour of Daylight Saving Time (DST), in java.time you get:
2 AM
moments.3 AM
moments.So two hours elapse between the requested 2 AM and the requested 3 AM.
LocalDateTime.atZone
while asking for 2 AM.LocalDateTime.atZone
with a call to ZonedDateTime.withLaterOffsetAtOverlap()
.LocalDateTime.atZone
while asking for 3 AM.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