I am trying to write a generic method to return a ZonedDateTime
given a date as String
and its format.
How do we make ZonedDateTime
to use the default ZoneId
if it is not specified in the date String
?
It can be done with java.util.Calendar
, but I want to use the Java 8 time API.
This question here is using a fixed time zone. I am specifying the format as an argument. Both the date and its format are String
arguments. More generic.
Code and output below:
public class DateUtil {
/** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone. */
public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
//use java.time from java 8
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
ZonedDateTime zonedDateTime = ZonedDateTime.parse(date, formatter);
return zonedDateTime;
}
public static void main(String args[]) {
DateUtil dateUtil = new DateUtil();
System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
}
}
Output
2017-09-14T15:00+05:30
Exception in thread "main" java.time.format.DateTimeParseException: Text '2017-09-14 15:00:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
at java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:1920)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1855)
at java.time.ZonedDateTime.parse(ZonedDateTime.java:597)
at com.nam.sfmerchstorefhs.util.DateUtil.parseToZonedDateTime(DateUtil.java:81)
at com.nam.sfmerchstorefhs.util.DateUtil.main(DateUtil.java:97)
Caused by: java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
at java.time.ZonedDateTime.from(ZonedDateTime.java:565)
at java.time.format.Parsed.query(Parsed.java:226)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
... 3 more
Caused by: java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type java.time.format.Parsed
at java.time.ZoneId.from(ZoneId.java:466)
at java.time.ZonedDateTime.from(ZonedDateTime.java:553)
... 5 more
A ZonedDateTime
needs a timezone or an offset to be built, and the second input doesn't have it. (It contains only a date and time).
So you need to check if it's possible to build a ZonedDateTime
, and if it's not, you'll have to choose an arbitrary zone for it (as the input has no indication about what's the timezone being used, you must choose one to be used).
One alternative is to first try to create a ZonedDateTime
and if it's not possible, then create a LocalDateTime
and convert it to a timezone:
public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
// use java.time from java 8
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
ZonedDateTime zonedDateTime = null;
try {
zonedDateTime = ZonedDateTime.parse(date, formatter);
} catch (DateTimeException e) {
// couldn't parse to a ZoneDateTime, try LocalDateTime
LocalDateTime dt = LocalDateTime.parse(date, formatter);
// convert to a timezone
zonedDateTime = dt.atZone(ZoneId.systemDefault());
}
return zonedDateTime;
}
In the code above, I'm using ZoneId.systemDefault()
, which gets the JVM default timezone, but this can be changed without notice, even at runtime, so it's better to always make it explicit which one you're using.
The API uses IANA timezones names (always in the format Region/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.
You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds()
.
If you want to use a specific timezone, just use ZoneId.of("America/New_York")
(or any other valid name returned by ZoneId.getAvailableZoneIds()
, New York is just an example) instead of ZoneId.systemDefault()
.
Another alternative is to use parseBest()
method, that tries to create a suitable date object (using a list of TemporalQuery
's) until it creates the type you want:
public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
// try to create a ZonedDateTime, if it fails, try LocalDateTime
TemporalAccessor parsed = formatter.parseBest(date, ZonedDateTime::from, LocalDateTime::from);
// if it's a ZonedDateTime, return it
if (parsed instanceof ZonedDateTime) {
return (ZonedDateTime) parsed;
}
if (parsed instanceof LocalDateTime) {
// convert LocalDateTime to JVM default timezone
LocalDateTime dt = (LocalDateTime) parsed;
return dt.atZone(ZoneId.systemDefault());
}
// if it can't be parsed, return null or throw exception?
return null;
}
In this case, I just used ZonedDateTime::from
and LocalDateTime::from
, so the formatter will try to first create a ZonedDateTime
, and if it's not possible, then it tries to create a LocalDateTime
.
Then I check what was the type returned and do the actions accordingly.
You can add as many types you want (all main types, such as LocalDate
, LocalTime
, OffsetDateTime
and so on, have a from
method that works with parseBest
- you can also create your own custom TemporalQuery
if you want, but I think the built-in methods are enough for this case).
When converting a LocalDateTime
to a ZonedDateTime
using the atZone()
method, there are some tricky cases regarding Daylight Saving Time (DST).
I'm going to use the timezone I live in (America/Sao_Paulo
) as example, but this can happen at any timezone with DST.
In São Paulo, DST started at October 16th 2016: at midnight, clocks shifted 1 hour forward from midnight to 1 AM (and the offset changes from -03:00
to -02:00
). So all local times between 00:00 and 00:59 didn't exist in this timezone (you can also think that clocks changed from 23:59:59.999999999 directly to 01:00). If I create a local date in this interval, it's adjusted to the next valid moment:
ZoneId zone = ZoneId.of("America/Sao_Paulo");
// October 16th 2016 at midnight, DST started in Sao Paulo
LocalDateTime d = LocalDateTime.of(2016, 10, 16, 0, 0, 0, 0);
ZonedDateTime z = d.atZone(zone);
System.out.println(z);// adjusted to 2017-10-15T01:00-02:00[America/Sao_Paulo]
When DST ends: in February 19th 2017 at midnight, clocks shifted back 1 hour, from midnight to 23 PM of 18th (and the offset changes from -02:00
to -03:00
). So all local times from 23:00 to 23:59 existed twice (in both offsets: -03:00
and -02:00
), and you must decide which one you want.
By default, it uses the offset before DST ends, but you can use the withLaterOffsetAtOverlap()
method to get the offset after DST ends:
// February 19th 2017 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 18th exist twice
LocalDateTime d = LocalDateTime.of(2017, 2, 18, 23, 0, 0, 0);
// by default, it gets the offset before DST ends
ZonedDateTime beforeDST = d.atZone(zone);
System.out.println(beforeDST); // before DST end: 2018-02-17T23:00-02:00[America/Sao_Paulo]
// get the offset after DST ends
ZonedDateTime afterDST = beforeDST.withLaterOffsetAtOverlap();
System.out.println(afterDST); // after DST end: 2018-02-17T23:00-03:00[America/Sao_Paulo]
Note that the dates before and after DST ends have different offsets (-02:00
and -03:00
). If you're working with a timezone that has DST, keep in mind that those corner-cases can happen.