6

I have a timestamp encoded as a String—for example, "2012-02-12T09:08:13.123456-0400", coming from an Oracle database.

The only way that I can think of reading this timestamp, is by using Timestamp.valueOf(), and that requires a format of yyyy-[m]m-[d]d hh:mm:ss[.f...]

I am convinced that this is the only way to read time without losing precision because other ways do not support nanosecond precision included in the example above (".123456").

With that in mind, I can simply trim the needed values, to fit the required format. Hence, the original string would be transformed:

  • Before: "2012-02-12T09:08:13.123456-0400"
  • After: "2012-02-12 09:08:13.123456"

If I do this, I remove the "-0400" timezone offset. This comes as a red flag to me, until I saw this post. One of the proposed answers states,

I think the correct answer should be java.sql.Timestamp is NOT timezone specific. Timestamp is a composite of java.util.Date and a separate nanoseconds value. There is no timezone information in this class. Thus just as Date this class simply holds the number of milliseconds since January 1, 1970, 00:00:00 GMT + nanos.

To prove to myself that the offset is not needed, I wrote a simple integration test.

Insert this timestamp into the database: "2015-09-08 11:11:12.123457". Read the database using Java, and print out the details. I get "2015-09-08 11:11:12.123457", which is the same value. This happens to be ok, since my JVM and the Oracle DB are running on the same machine.

  1. Is it a fact that a timezone is not factor in java.sql.Timestamp?
  2. Is there a better way to read that entire timestamp, without losing any precision in Java 7?
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
angryip
  • 2,140
  • 5
  • 33
  • 67
  • java.sql.Timestamp is timezone specific. From Javadocs : Sets the designated parameter to the given java.sql.Timestamp value, using the given Calendar object. The driver uses the Calendar object to construct an SQL TIMESTAMP value, which the driver then sends to the database. With a Calendar object, the driver can calculate the timestamp taking into account a custom timezone. If no Calendar object is specified, the driver uses the default timezone, which is that of the virtual machine running the application. – fg78nc Jun 15 '17 at 18:29
  • 2
    Your test appears to work because you are running it on a machine where the timezone offset is the same that appears in the original string. But if the string had a different offset, obviously your `Timestamp` value will not change, because you are ignoring the offset. The best solution depends on whether you are running Java 8 or later. Please specify. – erickson Jun 15 '17 at 18:38
  • @erickson Indeeed, I agee. && I am running java 7 – angryip Jun 15 '17 at 18:40
  • 2
    Of course timezone matters. An event happening at `2017-06-15 12:00:00` in London and an event happening at `2017-06-15 12:00:00` in New York are 5 hours apart. – Andy Turner Jun 15 '17 at 20:56

3 Answers3

3

tl;dr

org.threeten.bp.OffsetDateTime odt = 
    OffsetDateTime.parse(
        "2012-02-12T09:08:13.123456-0400",
        org.threeten.bp.format.DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ssZ" )  // Specify pattern as workaround for Java 8 bug in failing to parse if optional colon is not present.
    )
;

Using java.time

Rather than receiving a String from your database, you should retrieve an object, a date-time object, specifically a java.time object.

The java.time classes supplant the troublesome old date-time classes including java.sql.Timestamp. If your JDBC driver supports JDBC 4.2 and later, you can pass and receive java.time objects directly.

Instant

The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction). So this is equivalent to java.sql.Timestamp including support for the six digits of microseconds of your input data, so no precision lost per the requirements of your Question.

Instant instant = myResultSet.getObject( … , Instant.class ) ;

instant.toString(): 2012-02-12T13:08:13.123456Z

ZonedDateTime

If you want to see that same moment through the lens of a particular region's wall-clock time, apply a ZoneId to get a ZonedDateTime object.

ZoneId z = ZoneId.of( "America/St_Thomas" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

zdt.toString(): 2012-02-12T09:08:13.123456-04:00[America/St_Thomas]

OffsetDateTime

As for your direct Question of how to make sense of the string 2012-02-12T09:08:13.123456-0400 as a date-time value, parse as an OffsetDateTime.

A time zone has a name in the format of continent/region, and represents a history of past, present, and future changes to a region’s offset caused by anomalies such as Daylight Saving Time (DST). We have clue as to the time zone with your string, so we use OffsetDateTime rather than ZonedDateTime.

OffsetDateTime odt = OffsetDateTime.parse( "2012-02-12T09:08:13.123456-0400" ) ;

Well, that line of code above should have worked, but in Java 8 there is a small bug in parsing the offset lacking the optional COLON character between the hours and minutes. So -04:00 in Java 8 will parse but not -0400. Bug fixed in Java 9. Your String is indeed compliant with the ISO 8601 standard for date-time formats used by default in the java.time classes. Tip: Generally best to always format your offsets with the colon, and both hours/minutes and with a padding zero – I've seen other protocols and libraries expect only such full format.

Until you move to Java 9, specify the formatting pattern explicitly rather than rely on the implicit default pattern, as a workaround for this bug.

OffsetDateTime odt = 
    OffsetDateTime.parse(
        "2012-02-12T09:08:13.123456-0400",
        DateTimeFormatter.ofPattern( "yyyy-MM-dd'T'HH:mm:ssZ" )  // Specify pattern as workaround for Java 8 bug in failing to parse if optional colon is not present.
    )
;

Converting

If your JDBC driver is not yet compliant with JDBC 4.2, retrieve a java.sql.Timestamp object, for use only briefly. Immediately convert to java.time using new methods added to the old date-time classes.

java.sql.Timestamp ts = myResultSet.getTimestamp( … ) ;
Instant instant = ts.toInstant();

Proceed to do your business logic in java.time classes. To send a date-time back to the database convert from Instant to java.sql.Timestamp.

myPreparedStatement.setTimestamp( … , java.sql.Timestamp.from( instant ) ) ;

Java 6 & 7

In Java 6 & 7, the above concepts still apply, but java.time is not built-in. Use the ThreeTen-Backport library instead. To obtain, see bullets below.

In Java 7, you cannot use JDBC 4.2 features. So we cannot directly access java.time objects from the database through the JDBC driver. As seen above, we must convert briefly into java.sql.Timestamp from Instant. Call the utility methods DateTimeUtils.toInstant(Timestamp sqlTimestamp) & DateTimeUtils.toSqlTimestamp(Instant instant).

java.sql.Timestamp ts = myResultSet.getTimestamp( … ) ;
Instant instant = DateTimeUtils.toInstant( ts ) ;

…and…

java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( instant ) ;
myPreparedStatement.setTimestamp( … , ts ) ;

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.

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

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

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
0

java.sql.Timestamp.valueOf(String) parses the provided time in the current timezone. You can check this by looking at the implementation, which in the end simply calls Timestamp(int year, int month, int date, int hour, int minute, int second, int nano) which calls public Date(int year, int month, int date, int hrs, int min, int sec), which says (emphasis mine):

Allocates a Date object and initializes it so that it represents the instant at the start of the second specified by the year, month, date, hrs, min, and sec arguments, in the local time zone.

It is true that Timestamp doesn't have time zone information (it is just a wrapper with number of seconds since the GMT epoch + nanoseconds), but when loading or storing a Timestamp, JDBC will use the local (default) timezone, unless explicitly declared otherwise.

This means that a Timestamp at 10:00 in your local timezone will end up in the database as an SQL TIMESTAMP at 10:00, and not at - for example - 08:00 if your timezone is 2 hours ahead of GMT; and the reverse when loading.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
-1

You are correct that java.sql.Timestamp has no timezone information. Under the covers it is just an instant that is milliseconds since the epoch and the epoch itself is defined with essentially a 0 offset, January 1, 1970, 00:00:00 GMT.

That being said, when working with times, offset always matters and this will lead you into the hell, er, "dynamic world" that is ISO-8601 in Java 8 java.time. The offset matters a lot, unless you are standing in Greenwich, England. If you try to just ignore it you may sometimes guess the day wrong; it is Monday in Greenwich 8 hours earlier than it is Monday in Seattle, for example. I don't think Java8 has a built in DateTimeFormatter for the -0000 variants of ISO-8601, but if you are sure your format is "stable", this would work.

public void java8TimeWTF() {
  OffsetDateTime odt = OffsetDateTime.parse(
    "2012-02-12T09:08:13.123456-0400",
    DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nZ"));
  Instant i = odt.toInstant();
  System.out.printf("odt: %s, i: %s\n", odt, i);
}

Outputs odt: 2012-02-12T09:08:13.000123456-04:00, i: 2012-02-12T13:08:13.000123456Z

Here's what we had to do to deal with all the variant's of ISO-8601 coming out of our clients

import org.testng.annotations.Test;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import static org.testng.Assert.assertEquals;

public class Java8Time8601 {
  private final long EXPECTED_MILLIS = 1493397412000L;

  public Instant iso8601ToInstant(String s) {
    DateTimeFormatter[] dateTimeFormatters = {
            DateTimeFormatter.ISO_INSTANT,
            DateTimeFormatter.ISO_OFFSET_DATE_TIME,
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")
    };
    for (DateTimeFormatter dtf : dateTimeFormatters) {
      try {
        OffsetDateTime odt = OffsetDateTime.parse(s, dtf);
        Instant i = odt.toInstant();
        return i;
      } catch (DateTimeParseException dtpe) {
        ;
      }
    }
    throw new IllegalArgumentException(String.format("failed to parse %s", s));
  }

  @Test
  public void testInstantParse8601_Z() throws Exception {
    String[] candidates = {
            "2017-04-28T16:36:52.000Z",
            "2017-04-28T16:36:52.00Z",
            "2017-04-28T16:36:52.0Z",
            "2017-04-28T16:36:52Z",
            "2017-04-28T16:36:52+00:00",
            "2017-04-28T16:36:52-00:00",
            "2017-04-28T09:36:52-07:00",
            "2017-04-28T09:36:52-0700",
    };
    for (String candidate : candidates) {
      Instant i = iso8601ToInstant(candidate);
      assertEquals(i.toEpochMilli(), EXPECTED_MILLIS, String.format("failed candidate %s", candidate));
      System.out.println(i.toString());
    }
  }
}

There has got to be a better way.

Bob Kuhar
  • 10,838
  • 11
  • 62
  • 115
  • OP is using Java 7. – shmosel Jun 15 '17 at 21:14
  • Why are you ranting about Java 8's java.time, when this question is about `java.sql.Timestamp`. – Mark Rotteveel Jun 15 '17 at 21:14
  • @shmosel I didn't see that in there anywhere, my bad. In Java 7 JODA may be your only way out. It looks like Joda makes shorter work of it in any event, but seems to lose the nanoseconds – Bob Kuhar Jun 15 '17 at 21:24
  • @MarkRotteveel he was asking if he could ignore offset and how to parse. The DateTimeFormatter is all about parsing. I am, indeed, ranting as to how unnecessarily difficult that is in java.time. Carry on. – Bob Kuhar Jun 15 '17 at 21:33