Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting a date and time input in Eastern Time and converting to UTC timestamp in Java

I have a simple web interface that takes a date and time in the form of "2009/10/09 11:00" or "yyyy/MM/dd HH:mm". The time (from the user's standpoint) is in Eastern Time.

I want to be able to take this string, convert it to a UTC timestamp, so I can take this timestamp and query our NoSQL database based on the specified time.

My code is as follows:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
    LocalDateTime dateTime = LocalDateTime.parse(startSearchTime, formatter);
    System.out.println(dateTime);
    LocalDateTime utcTime = dateTime.plusHours(4);
    Instant instant = Instant.parse(utcTime.toString());
    System.out.println(instant.toEpochMilli());

I am getting the string from UI and storing it in 'startSearchTime'. I convert it from Eastern Time to UTC by adding 4 hours. I then attempt to create an instant object and parse the string and get epoch milliseconds but the exception I get is:

"Text '2015-10-16T14:00z' could not be parsed"

With this new Java 8 DateTime API, I thought this task would be easy, what am I missing?!

like image 963
smithzo622 Avatar asked Oct 16 '15 20:10

smithzo622


1 Answers

The answer by Yasmani Llanes is basically correct. I’ll expound.

LocalDateTime != UTC Moment

A LocalDateTime is not a real date-time, it is not tied to the time line. It has no real meaning until you adjust it into a time zone to determine a point on the time line, a moment. Your code, LocalDateTime utcTime, with your choice of variable name, shows you have conflated a "local" date-time with being a UTC moment. It is not. One is a vague idea, the other is real. (well, real in the Newton sense, not so much in the Einstein Relativistic sense ;-) )

So, the output of the LocalDateTime::toString is not a fully-formed string as expected by the Instant.parse method. Specifically, it has no data pertaining to an offset-from-UTC nor time zone. The previous paragraph explains why this is a feature not a bug.

What you want is a ZonedDateTime which is basically an Instant (a moment on the timeline in UTC) plus a ZoneId (a time zone).

ZonedDateTime = Instant + ZoneId

A time zone is an offset-from-UTC (hours and minutes) plus a set of rules and anomalies (such as Daylight Saving Time, DST) for past, present, and future adjustments.

ZoneId = offset-from-UTC + adjustment-rules

You are correct to go through LocalDateTime in the java.time framework, and this is where it gets a bit confusing. Logically, we should be able to parse directly from an input String to a ZonedDateTime. But there is the issue of an input string without any time-zone info may not be valid for a particular zone because of adjustment-rules. For example, in the Spring when we "spring-ahead" with Daylight Saving Time, in the United States jumping an hour ahead at the stroke of 2 AM, there is no "02:38" or "20:54" on that day. The clock jumps from 01:59.59.x to 03:00:00.0.

My understanding is that the java.time framework wants to handle this adjustment via a LocalDateTime object being passed to ZonedDateTime rather than have ZonedDateTime handle it directly while parsing. Two steps: (1) Parse string into LocalDateTime, (2) Feed LocalDateTime object and ZoneId object to ZonedDateTime. To correctly handle an input string with "20:54" that day, we need parse it as a LocalDateTime, then ask ZonedDateTime to use the specified time zone to make an adjustment (resulting in "03:54", I think -- read the class doc for details and logic used in adjustment behavior).

So we need to add to your code, calling ZonedDateTime. Using the LocalDateTime object you created, we need to specify a ZoneId object for ZonedDateTime to use in completing the transformation to a ZonedDateTime.

Proper Time Zone Names

You said the input string is in "Eastern Time". I'm afraid to tell you there is no such thing. The "EST", "EDT", and other such 3-4 letter codes are not official, not standardized, and not unique. You need to learn to use proper time zone names. Perhaps you mean America/New_York (note the underscore) or America/Montreal or some such zone. I will arbitrarily go with New York.

Variable Naming

Note how I've changed your variable names. Naming variables is generally quite important for clarity and later maintenance, but even more so for date-time work.

ISO 8601

By the way, a better way to exchange data of date-time values via strings is to use the ISO 8601 formats such as 2015-10-15T13:21:09Z. These formats include an offset-from-UTC such as the Z (Zulu, UTC) shown in previous sentence. The java.time framework wisely extends the ISO 8601 formats by appending the name of the time zone in brackets. Passing around date-time strings with no offset or time zone info is asking for trouble.

Example code.

Here is some sample code in Java 8. First we parse the string into a LocalDateTime object.

// Parse input string into a LocalDateTime object.
String input = "2009/10/09 11:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern ( "yyyy/MM/dd HH:mm" );
LocalDateTime localDateTime = LocalDateTime.parse ( input , formatter );

Transform that amorphous LocalDateTime into an actual moment on the time line by assigning a time zone. We assume that input string represents the wall-clock time in Poughkeepsie which uses the New York time zone. So we get a ZoneId object for New York time zone.

// Specify the time zone we expect is implied for this input string.
ZoneId zoneId = ZoneId.of ( "America/New_York" );
ZonedDateTime zdtNewYork = ZonedDateTime.of ( localDateTime , zoneId );

You can easily adjust into other time zones. I'll arbitrarily show India time as provides a contrast in two ways: ahead of UTC rather than behind, and its offset is not in whole hours (+05:30).

// For fun, adjust into India time, five and a half hours ahead of UTC.
ZonedDateTime zdtKolkata = zdtNewYork.withZoneSameInstant ( ZoneId.of ( "Asia/Kolkata" ) );

We can do date-time calculation like adding four hours. Because we have a ZonedDateTime, that class handles adjustments needed for anomalies such as Daylight Saving Time.

// Get a moment four hours later.
ZonedDateTime later = zdtNewYork.plusHours ( 4 );  // DST and other anomalies handled by ZDT when adding hours.

For a UTC time zone, you can go either of two ways.

  • Assign a time zone as you would for any other time zone, but note the handy constant defined in ZoneOffset (a subclass of ZoneId).
  • Or, alternatively, extract the Instant from within the ZonedDateTime. An Instant is always in UTC by definition.

Either way represents the same moment on the timeline. But notice in output below how each has a different format used by default in their respective toString implementation.

// To get the same moment in UTC time zone, either adjust time zone or extract Instant.
ZonedDateTime zdtUtc = zdtNewYork.withZoneSameInstant ( ZoneOffset.UTC );
Instant instant = zdtNewYork.toInstant ();

Dump to console.

System.out.println ( "input: " + input );
System.out.println ( "localDateTime: " + localDateTime );
System.out.println ( "zdtNewYork: " + zdtNewYork );
System.out.println ( "zdtKolkata: " + zdtKolkata );
System.out.println ( "zdtUtc: " + zdtUtc );
System.out.println ( "instant: " + instant );
System.out.println ( "later: " + later );

When run.

input: 2009/10/09 11:00
localDateTime: 2009-10-09T11:00
zdtNewYork: 2009-10-09T11:00-04:00[America/New_York]
zdtKolkata: 2009-10-09T20:30+05:30[Asia/Kolkata]
zdtUtc: 2009-10-09T15:00Z
instant: 2009-10-09T15:00:00Z
later: 2009-10-09T15:00-04:00[America/New_York]

Database Query

As for querying a database, search StackOverflow as that has been handled exhaustively already. Upshot: In the future JDBC should be able to use the java.time data types shown here. Until then, convert to a java.sql.Timestamp object. Convenient conversion methods provided for you, such as java.sql.Timestamp.from( Instant instant ).

java.sql.Timestamp ts = java.sql.Timestamp.from( zdtNewYork.toInstant () );
like image 194
Basil Bourque Avatar answered Nov 14 '22 23:11

Basil Bourque