Search code examples
javaspringspring-bootdstoffsetdatetime

Handling Daylight Saving Time with OffsetDateTime in Java when time needs to remain the same in local time regardless of DST


I'm using Spring Boot (Java 8) to develop the backend for a restaurant app which exposes some REST APIs. Everything related to dates and times is handled with OffsetDateTime objects and through the app I use, by default, the offset of "Europe/Rome". Those objects are also persisted in the DB. Everything is working fine, except... daylight saving time kicked in and now all the business hours of the restaurant are off by one hour.

This obviously happens because pre-DST the offset of "Europe/Rome" is +01:00, while post-DST is +02:00. So, to give you an example, the opening hour of the restaurant (saved inside the DB pre-DST) is 08:30:00+01:00, but it becomes 09:30:00+02:00 when accessed post-DST from the API (because it's automatically converted with the default zone of the system). I need the business hours of the restaurant to remain the same in local time, regardless of DST.

How can I handle this situation and offer consistent data to my users?


Solution

  • Offset versus time zone

    "Europe/Rome" is the name of a time zone, not an offset.

    An offset-from-UTC is simply a number of hours-minutes-seconds ahead of, or behind, the baseline of UTC. As you move eastward, local times are more and more hours ahead of UTC, while moving westward to the Americas the local times are more and more hours behind UTC.

    A time zone is much more. A time zone is a history of the past, present, and future changes to the offset used by the people of a particular region.

    Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

    You said:

    Everything related to dates and times is handled with OffsetDateTime objects

    No, you should not use that single class in all situations. OffsetDateTime class is for representing a moment where you know the offset but not the time zone. ZonedDateTime should be used when you know the time zone. And for moments specifically in UTC (an offset of zero), generally best to use Instant.

    The OffsetDateTime is rarely the right class to choose. Time zone is always preferable to a mere offset. So usually you will want either ZonedDateTime or Instant. Generally programmers and sysadmins should be thinking, working, logging, and debugging in UTC, so for that use Instant. For presentation to users in their expected time zone, or where business rules require, use ZonedDateTime. The one big exception is JDBC work for database access — the JDBC 4.2 specification inexplicably requires compliant drivers to support OffsetDateTime but not Instant nor ZonedDateTime. Fortunately conversion between the classes is quite simple and easy.

    Table of date-time types in Java, both modern and legacy

    Moment

    You need to understand the difference between a moment and not-a-moment. A moment is a specific point on the timeline. Forget about time zones and offsets, if someone in Casablanca Morocco calls someone in Tokyo Japan, they are sharing the same moment. To track a moment, use one of the three classes shown above in blue blocks: Instant, OffsetDateTime, ZonedDateTime.

    Instant phoneCallStartedUtc = Instant.now() ;  // A moment as seen in UTC.
    ZonedDateTime phoneCallStartedCasablanca = phoneCallStartedUtc.atZone( ZoneId.of( "Africa/Casablanca" ) ) ;
    ZonedDateTime phoneCallStartedTokyo = phoneCallStartedUtc.atZone( ZoneId.of( "Asia/Tokyo" ) ) ;
    

    See this code run live at IdeOne.com.

    phoneCallStartedUtc.toString(): 2021-03-28T20:33:58.695669Z
    phoneCallStartedCasablanca.toString(): 2021-03-28T21:33:58.695669+01:00[Africa/Casablanca]
    phoneCallStartedTokyo.toString(): 2021-03-29T05:33:58.695669+09:00[Asia/Tokyo]
    

    All three of those objects, phoneCallStartedUtc, phoneCallStartedCasablanca, and phoneCallStartedTokyo, represent the very same simultaneous moment. Their only difference is being viewed through different wall-clock time (what people see when they glance up at a clock on their wall).

    Not a moment

    Future appointments, such as when a restaurant will open, or when a patient will next see their dentist, cannot be tracked as a moment.

    We may know the date and time-of-day when we intend for the restaurant to open, but we cannot know the moment. That is because the clock time used in a time zone is defined by politicians.

    Politicians around the world have shown a penchant for changing the clock definition rules in the time zone(s) of their jurisdiction. They move the clock around as a political statement to distinguish themselves from neighboring jurisdictions, or the opposite, to align with neighbors. Politicians change their zone rules to manipulate financial markets. They move the clock during times of war and occupation for symbolic as well as logistical reasons. Politicians move the clock when they join in fads, such as Daylight Saving Time (DST). Later, they move their clocks again when they join a different fad, such as staying on DST year-round.

    The lesson to learn here is that time zone rules change, they change with surprising frequency, and they change with little or even no forewarning. So, for example, 3 PM on next Tuesday may not actually occur at the moment you expect right now on Wednesday. Next 3 PM might be an hour earlier than expect now, a half-hour later than you expect now, no one knows.

    To track future appointments that are tracked by the clock such as your restaurant opening, use LocalTime for the time of day, and LocalDate for the date. Where appropriate, you may combine those two into LocalDateTime object. Separately track the the intended time zone. Then at runtime when you need to calculate the scheduling of events, apply the time zone to the LocalDateTime to get a ZonedDateTime. The ZonedDateTime will determine a moment, but you cannot store that moment as politicians may move the clock on you, ripping the rug from beneath you.

    // Store these three values.
    ZoneId zoneIdRome = ZoneId.of( "Europe/Rome" ) ;
    LocalTime restaurantOpening = LocalTime.of( 9 , 30 ) ;  // 09:30 AM.
    LocalDate nextTuesday = LocalDate.now( zoneIdRome ).with( TemporalAdjusters.next( DayOfWeek.TUESDAY ) ) ;
    
    // Do NOT store this next value. Use this next value only on-the-fly at runtime.
    ZonedDateTime momentWhenRestaurantIsExpectedToOpenNextTuesday = 
        ZonedDateTime.of( nextTuesday , restaurantOpening , zoneIdRome ) ;
    

    See that code run live at IdeOne.com.

    restaurantOpening.toString(): 09:30
    nextTuesday.toString(): 2021-03-30
    momentWhenRestaurantIsExpectedToOpenNextTuesday.toString(): 2021-03-30T09:30+02:00[Europe/Rome]
    

    If you want to track when the restaurant actually did open, then you would be tracking moments. Then you would be using one of three classes discussed above, likely Instant.

    Instant momentWhenRestaurantActuallyOpened = Instant.now() ;  // 09:41 AM, eleven minutes late because the manager could not find the door keys that had dropped to the floor.
    

    By the way, some future appointments are not driven by the clock. For example, rocket launches are scheduled according to nature, the expected weather conditions. Politicians may alter the clock, change the time zone rules, but that does not change the moment when the rocket will launch. Such a change in time zone by the politicians will change how the media report on the planned launch, but the launch itself will not happen earlier or later. So for rocket launches and such we would use one of the three moment-oriented classes discussed above, almost certainly Instant class to focus on UTC only and avoid all time zones entirely.

    These concepts and classes have all been addressed many many times already on Stack Overflow. Search to learn more.