Trying to parse a datetime in PDT to a ZonedDateTi

2019-07-01 10:53发布

问题:

How should I parse this datetime value that is in the PDT timezone?

06/24/2017 07:00 AM (PDT)

I want to maintain the timezone so that I can then represent the time in other timezones depending on the website visitors preferences.

I tried using ZonedDateTime but I get a parse error:

   java.time.ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)")

The error is:

java.time.format.DateTimeParseException: Text '06/24/2017 07:00 AM (PDT)' could not be parsed at index 0
   at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
   at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
   at java.time.ZonedDateTime.parse(ZonedDateTime.java:597)
   at java.time.ZonedDateTime.parse(ZonedDateTime.java:582)   ... 29 elided

Also, do you agree that I should be using a ZonedDateTime?

回答1:

Since your format is non-standard, you need to specify it to the parser:

ZonedDateTime.parse(
    "06/24/2017 07:00 AM (PDT)", 
    DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm a (zzz)")
);


回答2:

The parse method expects a String in a specific format, like 2007-12-03T10:15:30+01:00[Europe/Paris]. As your input is in a different format, you need a DateTimeFormatter.

One detail to notice is that the API uses IANA timezones names (always in the format Continent/City, like America/Sao_Paulo or Europe/Berlin). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.

The API makes some exceptions with specific IDs and provides some defaults for them. For PDT, it defaults to America/Los_Angeles.

Another detail is that in the example below I used lowercase hh in the pattern: the format has AM/PM indication, so I think that hh is the correct pattern, as its value is from 1 to 12 (the common values when there's the AM/PM indicator).

If you use uppercase HH, it allows values from 0 to 23 (and it's not common to use this with AM/PM), and it will throw an exception if the input contains an hour like 07:00 PM.

So the code will be like:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm a (zzz)");
ZonedDateTime z = ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt);
System.out.println(z);

The output is:

2017-06-24T07:00-07:00[America/Los_Angeles]

But not all the 3-letter timezone names will be recognized by the API and will throw an exception.

Anyway, there are other timezones that also are in PDT (like America/Vancouver) - you can get a list of all by calling ZoneId.getAvailableZoneIds(). If you want to use a different timezone as the default, you can create a set of preferred zones and build a formatter with this set:

Set<ZoneId> preferredZones = new HashSet<>();
// set America/Vancouver as preferred zone
preferredZones.add(ZoneId.of("America/Vancouver"));
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // pattern
    .appendPattern("MM/dd/yyyy hh:mm a (")
    // append timezone with set of prefered zones
    .appendZoneText(TextStyle.SHORT, preferredZones)
    // finish the pattern
    .appendPattern(")")
    // create formatter
    .toFormatter();
System.out.println(ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt));

The API will use the preferred zones set (in this case, America/Vancouver) instead of the default (America/Los_Angeles). The output will be:

2017-06-24T07:00-07:00[America/Vancouver]


It's not clear where the input String's come from. If you can't control their format, then you have no choice: they need to be parsed this way. Then you can convert it to another timezone using the withZoneSameInstant method:

// parse the input string
ZonedDateTime z = ZonedDateTime.parse("06/24/2017 07:00 AM (PDT)", fmt);
// convert to another timezone
ZonedDateTime other = z.withZoneSameInstant(ZoneId.of("America/Sao_Paulo")); // 2017-06-24T11:00-03:00[America/Sao_Paulo]

The value of other will be 2017-06-24T11:00-03:00[America/Sao_Paulo].

But if you can control the output, it's always better (IMO) to internally work with UTC (java.time.Instant), and convert to some timezone only when displaying to users:

// convert ZonedDateTime to instant
ZonedDateTime z = // parse input
// convert to UTC (Instant is always in UTC)
Instant instant = z.toInstant();
// internally work with instant (as it's always in UTC)

// convert instant to some timezone only when necessary (like displaying to users)
ZonedDateTime converted = instant.atZone(ZoneId.of("Europe/London"));


回答3:

The error you get is well covered in the other answers already.

Also, do you agree that I should be using a ZonedDateTime?

Yes and no. Your string should definitely be parsed into a ZonedDateTime. I recommend you convert it to an Instant and store this. Then when you need to present it to a user according to his/her time zone preference, you may either convert the Instant to a ZonedDateTime again or just format it using a DateTimeFormatter with the desired default time zone.

Why do it this way? First, common practice is to store Instants. Some prefer to store just milliseconds since the epoch, I think this some (often misunderstood) performance measure. Certainly such milliseconds I quite unreadable while Instants can be deciphered on eye-sight, at least roughly. The only other alternative I respect is when you know for certain that your application will never need to be concerned with a time zone (does this ever happen?), then sometimes LocalDateTime is used for storage.

If I understand your situation correctly, you need to store the point in time for display into multiple time zones. You don’t need to store the time zone in which the time was originally entered (like PDT, except PDT is not really a full time zone). Instant is time zone neutral, which is one reason I prefer it over storing the time in some time zone, as ZonedDateTime would. Also an Instant is simpler conceptually, and my guess is that it is also simpler implementation-wise.

There are a couple of much better answers here: Best practices with saving datetime & timezone info in database when data is dependant on datetime.