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?!
The answer by Yasmani Llanes is basically correct. I’ll expound.
LocalDateTime
!= UTC MomentA 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
.
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.
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.
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.
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.
ZoneOffset
(a subclass of ZoneId
). 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]
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 () );