Converting time to UTC time goes the opposite way

2019-04-15 23:17发布

问题:

I'm trying to parse an offset time using Java 8 DateTimeFormatter.

I live in EST time which is UTC-5, so when I try to convert

2019-01-22T13:09:54.620-05:00 should be --> 2019-01-22T18:09:54.620

However, with my code, it gets the current time and goes back 5 hours, resulting in 2019-01-22 08:09:54.620

Code:


import java.sql.Timestamp
import java.time._
import java.time.format.DateTimeFormatter

import scala.util.{Failure, Success, Try}

class MyTimeFormatter(parser: DateTimeFormatter) {

   def parse(input: String): Try[Timestamp] = {
    Try(new Timestamp(Instant.from(parser.withZone(ZoneOffset.UTC).parse(input)).toEpochMilli))
  }
}

Test:

new MyTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSxxx")).parse("2019-01-22T13:09:54.620-05:00") shouldEqual Timestamp.valueOf("2019-01-22T18:09:54.620")

where parser is of type DateTimeFormatter and input string is just "2019-01-22T13:09:54.620-05:00"

I want to use this parser.parse method and not with specific temporalAccessors like OffsetDateTime.parse(input, parser) so I can handle all cases like LocalTime, LocalDateTime, ZonedDateTime, OffsetDateTime, etc..

It seems like the code just grabs the time, subtracts the offset, and brands it as UTC instead of calculating the offset with respect to UTC.

Also, is there a way to apply this UTC conversion only if the input format is of ZonedDateTime/OffsetDateTime format? If I input a LocalDateTime (which doesn't have an offset) such as 2017-01-01 12:45:00 the parser will still apply the UTC offset conversion because I told the parser to parse with zone UTC.

回答1:

tl;dr

Use modern java.time classes. Convert to legacy class only if necessary to work with old code.

Specifically, parse your input string as a OffsetDateTime object, adjust to UTC by extracting an Instant, and lastly, convert to java.sql.Timestamp (only if you must).

java.sql.Timestamp ts =                           // Avoid using this badly-designed legacy class if at all possible.
    Timestamp                                     // You can convert back-and-forth between legacy and modern classes.
    .from(                                        // New method added to legacy class to convert from modern class.
        OffsetDateTime                            // Represents a moment with an offset-of-UTC, a number of some hours-minutes-seconds ahead or behind UTC.
       .parse( "2019-01-22T13:09:54.620-05:00" )  // Text in standard ISO 8601 format can be parsed by default, without a formatting pattern.
       .toInstant()                               // Adjust from an offset to UTC (an offset of zero) by extracting an `Instant`. 
    )                                             // Returns a `Timestamp` object. Same moment as both the `OffsetDateTime` and `Instant` objects.
;

See this code run live at IdeOne.com, resulting in:

ts.toString(): 2019-01-22 18:09:54.62

If using JDBC 4.2 or later, skip the Timestamp altogether.

myPreparedStatement.setObject( … , myOffsetDateTime ) ;

Zulu

2019-01-22T13:09:54.620-05:00 should be --> 2019-01-22T18:09:54.620

If you meant that second value to represent a moment in UTC, append the offset-from-UTC to indicate that fact. Either +00:00 or Z (pronounced “Zulu”): 2019-01-22T18:09:54.620Z.

Reporting a moment without an offset-from-UTC or time zone indicator is like reporting an amount of money without a currency indicator.

OffsetDateTime

A string with an offset-from-UTC should be parsed as a OffsetDateTime object.

Your input string happens to comply with the ISO 8601 standard formats for textual date-time values. The java.time classes use ISO 8601 formats by default when parsing/generating strings. So no need to specify a formatting pattern.

OffsetDateTime odt = OffsetDateTime.parse( "2019-01-22T13:09:54.620-05:00" ) ;

Timestamp

Apparently you want a java.sql.Timestamp object. This is one of the terrible date-time classes bundled with the earliest versions of Java. These classes are now legacy, supplanted entirely by the modern java.time classes with the adoption of JSR 310. Avoid these legacy classes whenever possible.

If you must have a Timestamp to interoperate with old code not yet updated to work with java.time, you can convert. To convert, call new methods added to the old classes.

Instant

The java.sql.Timestamp class carries a from( Instant ) method. An Instant is a moment in UTC. To adjust from the offset of our OffsetDateTime to UTC, just extract an Instant.

Instant instant = odt.toInstant() ;
java.sql.Timestamp ts = Timestamp.from( instant ) ;

We have three objects ( odt, instant, & ts ) that all represent the same moment. The first has a different wall-clock time. But all three are the same simultaneous point on the timeline.

JDBC 4.2

As of JDBC 4.2, we can directly exchange java.time objects with the database. So no need to use Timestamp.

myPreparedStatement.setObject( … , odt ) ;

…and…

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

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.

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.

Where to obtain the java.time classes?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
    • Java 9 adds some minor features and fixes.
  • Java SE 6 and Java SE 7
    • Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
  • Android
    • Later versions of Android bundle implementations of the java.time classes.
    • For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….

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.



回答2:

While I cannot reproduce your issue precisely (even with changing my clock to EST), this is what I am observing:

Instant instant = Instant.from(parser.withZone(ZoneOffset.UTC).parse("2019-01-22T13:09:54.620-05:00"));

This is producing the time you would expect (2019-01-22T18:09:54.620Z).

Timestamp ts = new Timestamp(instant.toEpochMilli());

Because this is based on java.util.Date, which displays as your local time.

A better way to convert an Instant to a Timestamp is via the LocalDateTime, like so:

Timestamp ts = Timestamp.valueOf(instant.atZone(ZoneOffset.UTC).toLocalDateTime());