60

I have Java 8 Spring web app that will support multiple regions. I need to make calendar events for a customer location. So let's say my web and Postgres server is hosted in MST timezone (but I guess it could be anywhere if we go cloud). But the customer is in EST. So, following some best practices I read, I thought I would store all date times in UTC format. All date-time fields in the database are declared as TIMESTAMP.

So here is how I take a LocalDateTime and convert it to UTC:

ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(dto.getStartDateTime(), ZoneOffset.UTC, null);
//appointment startDateTime is a LocalDateTime
appointment.setStartDateTime( startZonedDT.toLocalDateTime() );

Now, for example, when a search for a date comes in, I have to convert from the requested date time to UTC, get the results, and convert to the user timezone (stored in the DB):

ZoneId  userTimeZone = ZoneId.of(timeZone.getID());
ZonedDateTime startZonedDT = ZonedDateTime.ofLocal(appointment.getStartDateTime(), userTimeZone, null);
dto.setStartDateTime( startZonedDT.toLocalDateTime() );

Now, I am not sure this is the correct approach. I also wonder if because I am going from LocalDateTime to ZonedDateTime and vice versa, I might be losing any timezone info.

Here is what I am seeing and it doesn't seem correct to me. When I receive the LocalDateTime from the UI, I get this:

2016-04-04T08:00

The ZonedDateTime =

dateTime=2016-04-04T08:00
offset="Z"
zone="Z"

And then when I assign that converted value to my appointment LocalDateTime I store:

2016-04-04T08:00

I feel like because I am storing in LocalDateTime I am losing the timezone that I converted into ZonedDateTime.

Should I make my entity (appointment) use ZonedDateTime instead of LocalDateTime so that Postgres doesn't lose that information?

---------------- EDIT ----------------

After Basil's excellent answer, I realized that I have the luxury of not caring about the user's timezone - all appointments are against a specific location so I can store all date times as UTC and then convert them to the location timezone when retrieved. I made the following follow-up question

Saikat
  • 14,222
  • 20
  • 104
  • 125
sonoerin
  • 5,015
  • 23
  • 75
  • 132
  • Your app needs timezone information? – Sanj Apr 05 '16 at 05:03
  • 1
    Regarding your "Edit" section, know that politicians *love* to redefine time zones, offsets, Daylight Saving Time (DST), etc. So never go *too* far into the future with UTC values. If you mean dental hygiene appointments made far in advance, ex. “3 PM on December 5th, 2017 later this year”, then you may indeed want to use a `LocalDateTime` to store that fact. The intent there is 3 PM however politicians may have redefined that moment by then. For a schedule you might temporarily apply time zone to to the potential `LocalDateTime` to generate an actual moment as a `ZonedDateTime`/`Instant`. – Basil Bourque Mar 02 '17 at 00:15

3 Answers3

130

Postgres has no such data type as TIMESTAMP. Postgres has two types for date plus time-of-day: TIMESTAMP WITH TIME ZONE and TIMESTAMP WITHOUT TIME ZONE. These types have very different behavior with regard to time zone information.

  • The WITH type uses any offset or time zone information to adjust the date-time to UTC, then disposes of that offset or time zone; Postgres never saves the offset/zone info.
    • This type represents a moment, a specific point on the timeline.
  • The WITHOUT type ignores any offset or zone info that may be present.
    • This type does not represent a moment. It represents a vague idea of potential moments along a range of about 26-27 hours (the range of time zones around the globe).

You virtually always want the WITH type, as explained here by expert David E. Wheeler. The WITHOUT only makes sense when you have the vague idea of a date-time rather than a fixed point on the timeline. For example, "Christmas this year starts at 2016-12-25T00:00:00" would be stored in the WITHOUT as it applies to any time zone, not yet having been applied to any one single time zone to get an actual moment on the timeline. If Santa’s elves were tracking the start time for Eugene Oregon US, then they would use the WITH type and an input that included an offset or time zone such as 2016-12-25T00:00:00-08:00 which gets saved into Postgres as 2016-12-25T08:00.00Z (where the Z means Zulu or UTC).

The equivalent of Postgres’ TIMESTAMP WITHOUT TIME ZONE in java.time is java.time.LocalDateTime. As your intention was to work in UTC (a good thing), you should not be using LocalDateTime (a bad thing). That may be the main point of confusion and trouble for you. You keep thinking about using LocalDateTime or ZonedDateTime but you should be using neither; instead you should be using Instant (discussed below).

I also wonder if because I am going from LocalDateTime to ZonedDateTime and vice versa, I might be losing any timezone info.

Indeed you are. The entire point to LocalDateTime is to lose time zone info. So we rarely use this class in most apps. Again, the Christmas example. Or another example, "Company policy: All our factories around the world take lunch at 12:30 PM". That would be LocalTime, and for a particular date, LocalDateTime. But that has no real meaning, not an actual point on the timeline, until you apply a time zone to get a ZonedDateTime. That lunch break will be at different points on the timeline in the Delhi factory than the Düsseldorf factory and different again at the Detroit factory.

The word "Local" in LocalDateTime may be counter-intuitive as it means no particular locality. When you read “Local” in a class name, think “Not a moment… not on the timeline… just a fuzzy idea about a kinda-sorta date-time”.

Your servers should almost always be set to UTC in their operating system time zone. But your programming should never depend on this externality as it is all too easy for a sysadmin to change it or for any other Java app to change the current default time zone within the JVM. So always specify your desired/expected time zone. (Same goes for Locale, by the way.)

Upshot:

  • You are working way too hard.
  • Programmers/sysadmins must learn to "Think global, Present local".

During the work day while wearing your geek hard-hat, think in UTC. Only at the end of the day when switching to your layperson’s had should you go back to thinking of the local time of your town.

Your business logic should focus on UTC. Your database storage, business logic, data exchange, serialization, logging, and your own thinking should all be done in UTC time zone (and 24-hour clock, by the way). When presenting data to users, only then apply a particular time zone. Think of zoned date-times as an external thing, not a working part of your app’ internals.

On the Java side, use java.time.Instant (a moment on the timeline in UTC) in much of your business logic.

Instant now = Instant.now();

Hopefully JDBC drivers will eventually be updated to deal with java.time types like Instant directly. Until then we must use java.sql types. The old java.sql class have new methods for conversion to/from java.time.

java.sql.TimeStamp ts = java.sql.TimeStamp.valueOf( instant );

Now pass that java.sql.TimeStamp object via setTimestamp on a PreparedStatement to be saved to a column defined as TIMESTAMP WITH TIME ZONE in Postgres.

To go the other direction:

Instant instant = ts.toInstant();

So that is easy, going from Instant to java.sql.Timestamp to TIMESTAMP WITH TIME ZONE, all in UTC. No time zones involved. The current default time zone of your server OS, your JVM, and your clients, is all irrelevant.

To present to user, apply a time zone. Use proper time zone names, never the 3-4 letter codes such as EST or IST.

ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.ofInstant( instant , zoneId );

You can adjust into a different zone as needed.

ZonedDateTime zdtKolkata = zdt.withZoneSameInstant( ZoneId.of( "Asia/Kolkata" ) );

To get back to an Instant, a moment on the timeline in UTC, you can extract from the ZonedDateTime.

Instant instant = zdt.toInstant();

No where in there did we use LocalDateTime.

If you do get a piece of data without any offset-from-UTC or time zone, such as 2016-04-04T08:00, that data is entirely useless to you (assuming we are not talking about the Christmas or Company Lunch type scenarios discussed above). A date-time without offset/zone info is like a monetary amount without indicating currency: 142.70 or even $142.70 -- useless. But USD 142.70, or CAD 142.70, or MXN 142.70… those are useful.

If you do get that 2016-04-04T08:00 value, and you are absolutely certain of the intended offset/zone context, then:

  1. Parse that string as a LocalDateTime.
  2. Apply an offset-from-UTC to get a OffsetDateTime, or (better) apply a time zone to get a ZonedDateTime.

Like this code.

LocalDateTime ldt = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "Asia/Kolkata" ); // Or "America/Montreal" etc.
ZonedDateTime zdt = ldt.atZone( zoneId ); // Or atOffset( myZoneOffset ) if only an offset is known rather than a full time zone.

Your Question really is a duplicate of many others. These issues have been discussed many times in other Questions and Answers. I urge you to search and study Stack Overflow to learn more on this topic.

JDBC 4.2

As of JDBC 4.2 we can directly exchange java.time objects with the database. No need to ever use java.sql.Timestamp again, nor its related classes.

Storing, using OffsetDateTime as defined in the JDBC spec.

myPreparedStatement.setObject( … , instant.atOffset( ZoneOffset.UTC ) ) ;  // The JDBC spec requires support for `OffsetDateTime`. 

…or possibly use Instant directly, if supported by your JDBC driver.

myPreparedStatement.setObject( … , instant ) ;  // Your JDBC driver may or may not support `Instant` directly, as it is not required by the JDBC spec. 

Retrieving.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;

Table of date-time types in Java (both legacy and modern) and in standard SQL


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes. Hibernate 5 & JPA 2.2 support java.time.

Where to obtain the java.time classes?

Table of which java.time library to use with which version of Java or Android

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thank you very much for the excellent answer. I had read many posts and articles on the best approach for handling timezones in a web app. I have the luxury of knowing that all times used are for a specific location, regardless of where the user system clock says they are at. I do have a follow up question on my question, if you don't mind. Again, thank you for such a detailed and helpful answer. – sonoerin Apr 07 '16 at 12:41
  • I must read this again when I'll have the time. Very nice explanation. So a LocalTime for example indicates the pure "TIME" value, like a sign on a shop door indicating "We are open from 8:00 to 19:00" ← this doesn't care about daylight savings for example, 8 is 8 in every day of the year for shop owner and customers. While when we need to store a reference to an event, we must send it's time with time zone to PostgresSQL - it will save it in UTC. Finally when us or another user would need to see this time, it will be taken in UTC, and adapted to the final user time zone..is it right? – funder7 Oct 23 '20 at 22:36
  • 1
    @funder7 (A) Yes, `LocalTime` represents a time-of-day, nothing more, nothing less. (B) Booking an event or appointment in the future is a bit complicated, to account for possible changes in zone rules. If you mean "dentist appt next January 21, 2021 at 3 PM" you store the date & time in Postgres type `TIMESTAMP WITHOUT TIME ZONE`. Separately in a second column of text type, you store the ID of the intended time zone, such as `America/Chicago`. When building a calendar for presentation to user, at runtime extract the 1st as `LocalDateTime` & 2nd as `ZoneId`, combine to form `ZonedDateTime`. – Basil Bourque Oct 24 '20 at 04:16
  • @BasilBourque thanks for the info. I'm trying to understand this topic at 110%. I read that the best practice to store an appointment for example, is to always save UTC timestamps (and I suppose that an `Instant` whould do the job). We are talking about `TIMESTAMP WIT TIME ZONE` used on the db for this information. Finally an user, in any part of the world, should be able to understand when an event occours: it's up to the scope of the application to convert the UTC into the appointment location timezone, or the user current location timezone. – funder7 Oct 24 '20 at 14:11
  • Let's say that we have an appointment in Milan(Italy) happening on 01/01/2021 21:00 "Europe/Rome", it's saved on the db in UTC, as all the other data. Now a user living in NYC needs to understand when this appointment will take place: we can show him the datetime converted to "America/New_York" TZ, or instead, in "Europe/Rome" TZ. Once the user will fly from NYC to Milan, he will find both info useful. The point is to store everything converted into the same TZ reference (UTC), and the manipulate the datetime depending on the goal you have. Makes sense or there is something wrong/missing? – funder7 Oct 24 '20 at 14:18
  • @funder7 No, **never save future appointments in UTC**. You would be saving a specific moment, a specific point on the timeline. Appointments are a date with a specific time-of-day, **not a specific moment**. That time-of-day in the future will become another different moment if the politicians change the time zone rules. And politicians around the world do change the rules with surprising frequency. After extracting your `TIMESTAMP WITHOUT TIME ZONE` as a `LocalDateTime`, and applying the originally intended time zone `ZoneId` to get a `ZonedDateTime`, you can adjust into a second time zone. – Basil Bourque Oct 24 '20 at 16:04
  • @funder7 Whoever told you to save future appointments in UTC is incorrect; that is a worst practice, not a best practice. Store in database using a type akin to the SQL-standard `TIMESTAMP WITHOUT TIME ZONE`, not `TIMESTAMP WITH TIME ZONE`. Use a second column to record the intended time zone identifier. If, after the event, you want to record the moment when the activity actually began, or ended, or was paid for, you have a known specific moment, so for that you would use a single column of type `TIMESTAMP WITH TIME ZONE` for UTC rather than `TIMESTAMP WITHOUT TIME ZONE`. – Basil Bourque Oct 24 '20 at 16:14
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/223557/discussion-between-basil-bourque-and-funder7). – Basil Bourque Oct 24 '20 at 16:25
  • 1
    @funder7 Suppose you book your next dental appointment For October 15 of the following year. When you booked the appointment, your country observed Daylight Saving Time (DST) beginning on last day of September. And then your country decides to change the rule so DST begins on the last day of October. At what time of day of day should you appear at the dentist office? An appointment recorded in UTC would be wrong, off by the hour of DST offset. – Basil Bourque Oct 24 '20 at 16:33
  • @funder7 I posted [a Question](https://stackoverflow.com/q/64545555/642706) and [an Answer](https://stackoverflow.com/a/64545556/642706) on Stack Overflow with your specific example problem. – Basil Bourque Oct 26 '20 at 21:58
  • Great answer as always @BasilBourque (learned a lot from you, thanks!). One question though `java.time.Instant` is not timezone aware why are we saying it's comparable with `timestamptz`? – Saikat Aug 03 '23 at 09:29
12

TLDR:

To convert from LocalDateTime to ZonedDateTime the following code. You can use .atZone( zoneId ), a comprehensive list of zoneIDs are found in the column TZ database name on Wikipedia.

LocalDateTime localDateTime = LocalDateTime.parse( "2016-04-04T08:00" );
ZoneId zoneId = ZoneId.of( "UTC" ); // Or "Asia/Kolkata" etc.
ZonedDateTime zdt = localDateTime.atZone( zoneId );
Whitecat
  • 3,882
  • 7
  • 48
  • 78
0
public ZonedDateTime transformToZonedDateTime(String date, String timeZone) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
    LocalDate localDate = LocalDate.parse(Date, formatter);
    LocalDateTime localDateTime = localDate.atTime(LocalTime.now());
    ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneOffset.UTC);
    ZoneId zoneId = ZoneId.of(timeZone);
    return zonedDateTime.withZoneSameInstant(zoneId);
}

INPUT: date = "18-05-2023" and timeZone = "Asia/Hong_Kong"

RETURNS: "2023-05-19T20:43Z"

Shafeeq Mohammed
  • 1,193
  • 17
  • 25