store Booking – in the sense of “Online booking” (so participants that are from different countries/time zones needs meet in the same moment on the timeline). I tend to use LocalDateTime, since DB and Backend is set to UTC and since incoming “create booking” json message contains ISO 8601 (with offset) startDateTime & endDateTime fields.
You did not really define what you mean by "booking".
If what you meant is to pinpoint a moment, a specific point on the timeline, then LocalDateTime
is exactly the wrong class to use.
For example, suppose a missile launching company has offices in Japan, Germany, and Houston Texas US. Staff at all offices want to be in a group conference call during a launch. We would specify the moment of launch with an offset of zero from UTC. For that we use Instant
class. The Z
in the string below means an offset of zero, +00:00
, and is pronounced “Zulu”.
Instant launch = Instant.parse( "2022-05-23T05:31:23.000Z" ) ;
Let's tell each office their deadline to join the call.
ZonedDateTime tokyo = launch.atZone( ZoneId.of( "Asia/Tokyo" ) ) ;
ZonedDateTime berlin = launch.atZone( ZoneId.of( "Europe/Berlin" ) ) ;
ZonedDateTime chicago = launch.atZone( ZoneId.of( "America/Chicago" ) ) ; // Houston time zone is "America/Chicago".
We have four date-time objects, all representing the very same simultaneous moment.
As for the current default time zone of your database session and of your backend server, those should be irrelevant to your programming. As a programmer, those are out of your control, and are subject to change. So always specify your desired/expected time zone.
As for the rest of your Question, I cannot discern a specific question. So all I can do is comment on your examples.
LocalTime
Company has a policy that lunchtime starts at 12:30 PM at each of its factories worldwide.
Yes, LocalTime
is correct for representing a time-of-day for any place in the world.
Let's ask if lunch has started at the factory in Delhi India.
LocalTime lunchStart = LocalTime.of( 12 , 30 ) ;
ZoneId zKolkata = ZoneId.of( "Asia/Kolkata" ) ;
ZonedDateTime nowKolkata = ZonedDateTime.now( zKolkata ) ;
boolean isBeforeLunchKolkata = nowKolkata.toLocalTime().isBefore( lunchStart ) ;
Actually, we have a subtle bug there. On some dates in some zones, the time 12:30
may not exist. For example, suppose that time zone on that date has a Daylight Saving Time (DST) cutover, "Springing ahead" one hour at noon to 1 PM. In that case, there would be no 12:30. The time 13:30 would be the "new 12:30".
To fix this bug, let's adjust from nowKolkata
to use a different time of day. During this adjustment, java.time considers any anomalies such as DST, and handles them.
Note that this moving to a new time-of-day results in a new fresh separate ZonedDateTime
. The java.time classes use immutable objects.
ZonedDateTime lunchTodayKolkata = nowKolkata.with( lunchStart ) ;
Now we should rewrite that test for whether lunch has started.
boolean isBeforeLunchKolkata = nowKolkata.isBefore( lunchTodayKolkata ) ;
Let's ask how long until lunch starts. Duration
class represents a span of time not attached to the timeline, on the scale of hours-minutes-seconds.
Duration duration = Duration.between( nowKolkata , lunchTodayKolkata ) ;
We can use that duration as another way of determining if lunch has started there at that factory. If the duration is negative, we know we passed the start of lunch — meaning we would have to go back in time to see lunch start.
boolean isAfterLunchKolkata = duration.isNegative() ;
boolean isBeforeLunchKolkata = ! duration.isNegative() ;
LocalDate
The LocalDate
class represents a date only, without a time-of-day, and without the context of a time zone or offset-from-UTC.
Be aware that for any given moment the date varies around the globe by zone. So right now it can be “tomorrow” in Tokyo Japan while simultaneously “yesterday” in Toledo Ohio US. So a LocalDate
is inherently ambiguous with regard to the timeline.
Birthday - if it were crucial to know someone’s age to the very day, then a date-only value is not enough. With only a date, a person appearing to turn 18 years old in Tokyo Japan would still be 17 in Toledo Ohio US.
If you want to be precise about someone's age, you need their time of birth in addition to the date of birth and time zone of birth. Just the date and time zone of their birth place is not enough to narrow down their age to the first moment of their 18th year.
The time zone for Toledo Ohio US is America/New_York
.
LocalDate birthDate = LocalDate.of( 2000 , Month.JANUARY , 23 ) ;
LocalTime birthTime = LocalTime.of( 3 , 30 ) ;
ZoneId birthZone = ZoneId.of( "America/New_York" ) ;
ZonedDateTime birthMoment = ZonedDateTime.of( birthDate , birthTime , birthZone ) ;
To determine if the person is 18 years of age precisely, add 18 years to that moment of birth. Then compare to current moment.
ZonedDateTime turning18 = birthMoment.plusYears( 18 ) ;
ZonedDateTime nowInBirthZone = ZonedDateTime.now( birthZone ) ;
boolean is18 = turning18.isBefore( nowInBirthZone ) ;
Due date in a contract. If stated as only a date, a stakeholder in Japan will see a task as overdue while another stakeholder in Toledo sees the task as on-time.
True.
A contract needing moment precision must state a date, a time-of-day, and a time zone.
Let's say a contract expires after the 23rd of May next year as seen in Chicago IL US.
I suggest avoiding the imprecise notion of "midnight". Focus on the first moment of a day on a certain date in a certain zone. So "after the 23rd" means the first moment of the 24th.
Some dates in some zones may start a time other than 00:00. Let java.time determine the first moment of a day.
LocalDate ld = LocalDate.of( 2023 , Month.MAY , 23 ) ;
ZoneId zChicago = ZoneId.of( "America/Chicago" ) ;
ZonedDateTime expiration = ld.plusDays( 1 ).atStartOfDay( zChicago ) ;
boolean isContractInEffect = ZonedDateTime.now( zChicago ).isBefore( expiration ) ;
LocalDateTime
Christmas starts at midnight on the 25th of December 2015.
Use LocalDateTime
when you mean a date and time as seen in any locality.
Christmas starts at the first moment of the 25th of December in 2015 in every locality. So yes, use LocalDateTime
.
LocalDateTime xmas2015 = LocalDateTime.of( 2015 , Month.DECEMBER , 25 , 0 , 0 , 0 , 0 ) ;
That class purposely lacks the context of a time zone or offset-from-UTC. So an object of this class is inherently ambiguous with regard to the timeline. This class cannot represent a moment.
In other words, Santa arrives in Kiribati (the most advanced time zone, at 14 hours ahead of UTC) before arriving in Japan, and arrives still later in India, still later in Europe, and so on. The red sleigh chases each new day dawning successively in zone after zone around the world all night long.
Let's determine the moment Christmas started then in Jordan.
ZoneId zAmman = ZoneId.of( "Asia/Amman" ) ;
ZonedDateTime xmasAmman = xmas2015.atZone( zAmman) ;
Adjust that moment to the time zone for Denver Colorado US.
ZoneId zDenver = ZoneId.of( "America/Denver" ) ;
ZonedDateTime xmasStartingInAmmanAsSeenInDenver = xmasAmman.withZoneSameInstant( zDenver ) ;
If you interrogate that xmasStartingInAmmanAsSeenInDenver
object, you will see that when Christmas was starting in Jordan, the date in Denver was still the 24th, the day before Christmas. Christmas would not reach Colorado for several more hours.
Booking – in the sense of “local” dentist appointment
Appointments in the future that are intended to stick with their assigned time-of-day without regard to any changes to the time zone's rules must be tracked as three separate pieces:
- Date with time-of-day
- Time zone by which to interpret that date & time.
- Duration, for how long the appointment will last.
Let's book an appointment for next January 23rd at 3 PM in New Zealand.
LocalDateTime ldt = LocalDateTime.of( 2023 , 1 , 23 , 15 , 0 , 0 , 0 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
Duration d = Duration.ofHours( 1 ) ;
Each of those three items must be stored in your database.
- The first can be stored in a column of a type akin to the SQL standard type
TIMESTAMP WITHOUT TIME ZONE
. Notice the "WITHOUT", not "WITH".
- The time zone can be stored as text, by its standardized name in format of
Continent/Region
. Never use 2-4 letter pseudo-zones such as IST
, CST
, etc.
- I would store the duration in standard ISO 8601 format,
PnYnMnDTnHnMnS
. The P
marks the beginning, which the T
separates the two portions. So 1 hour is PT1H
.
When you need to create a schedule, you must determine a moment. To do that, combine your parts.
LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ) ;
ZoneId z = ZoneId.of( myResultSet.getString( … ) ) ;
Duration d = Duration.parse( myResultSet.getString( … ) ) ;
ZonedDateTime start = ldt.atZone( z ) ;
ZonedDateTime end = start.plus( d ) ;
I and others have explained this multiple times, so search to learn more. The key point is that politicians around the world have shown a penchant for frequently changing the rules of the time zones in their jurisdictions. And they do so with surprisingly little forewarning. So the moment of that 3PM appointment might come an hour earlier that you expect now, or a half-hour later, or who knows what. Expecting your time zones of interest to remain stable is to doom your software to an eventual fail. You may think me paranoid. Let's check back in some years… when all the customers in your booking app are arriving at the wrong time.
Instant
Some critical function must start every day at 1:30am (LocalDateTime & ZonedDateTime are wrong in this case, because e.g. during switching from/to standard time to summer time, function runs twice/not at all).
No, incorrect, not Instant
. If you want to run a task at 1:30 AM daily, you need to use LocalTime
combined with a ZoneId
. This is similar in concept to the appointments discussion directly above.
If we want the server in a Chicago office to run that task in the wee hours, we would define the date-time and zone.
ZoneId z = ZoneId.of( "America/Chicago" ) ;
LocalTime targetTime = LocalTime.of( 1 , 30 ) ;
To determine the next run, capture the current moment.
ZonedDateTime now = ZonedDateTime.now( z ) ;
See if we are before the target time, then calculate time to wait. We use "not after" as a shorter way of saying "if before or equal to". Think about if the current moment happens to be exactly 1:30:00.000000000 AM.
if( ! now.toLocalTime().isAfter( targetTime ) ) { // If before or equal to the target time.
Duration d = Duration.between( now , now.with( targetTime ) ) ;
…
}
If the target time has passed for today, add a day. Then calculate time to elapse until the next run.
else { // Else, after target time. Move to next day.
LocalDate tomorrow = now.toLocalDate().plusDays( 1 ) ;
ZonedDateTime nextRun = ZonedDateTime.of( tomorrow , targetTime , z ) ;
Duration d = Duration.between( now , nextRun ) ;
…
}
The ZonedDateTime.of
method automatically adjusts if your specified time-of-day does not exist on that date in that zone.
If you are using an executor service to run your task at that same local time, then you cannot use ScheduledExecutorService
with the methods scheduleAtFixedRate
or scheduleWithFixedDelay
. Use the schedule
method with a single delay.
Pass a reference to that executor service to the constructor of your task, your Runnable
/Callable
. Then write your task in such as way that the last thing it does in submit itself to the executor service with a fresh newly-calculated delay. On most days that delay will be 24 hours. But on anomalous days such as Daylight Saving Time (DST) cutover, the delay might be something like 23 hours or 25 hours. So each task run schedules the next task run.