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 aLocalDateTime
and convert it to a timezone: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
, likeAmerica/Sao_Paulo
orEurope/Berlin
). Avoid using the 3-letter abbreviations (likeCST
orPST
) 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 byZoneId.getAvailableZoneIds()
, New York is just an example) instead ofZoneId.systemDefault()
.Another alternative is to use
parseBest()
method, that tries to create a suitable date object (using a list ofTemporalQuery
's) until it creates the type you want:In this case, I just used
ZonedDateTime::from
andLocalDateTime::from
, so the formatter will try to first create aZonedDateTime
, and if it's not possible, then it tries to create aLocalDateTime
.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 afrom
method that works withparseBest
- you can also create your own customTemporalQuery
if you want, but I think the built-in methods are enough for this case).Daylight Saving Time
When converting a
LocalDateTime
to aZonedDateTime
using theatZone()
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: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 thewithLaterOffsetAtOverlap()
method to get the offset after DST ends: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.