String of date to OffsetDateTime

wazza picture wazza · Aug 10, 2017 · Viewed 9.8k times · Source

I am converting string of date/datetime to OffsetDateTime and I have datetime format which may have one of these values

yyyy-MM-dd, yyyy/MM/dd

Sometimes with and without time and I need to convert this into OffsetDateTime.

I have tried below code

// for format yyyy-MM-dd
DateTimeFormatter DATE_FORMAT = new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd")
                        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
                        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
                        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
                        .parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
                        .toFormatter();

As it doesn't have time I am setting this to default values but when I try to parse

OffsetDateTime.parse("2016-06-06", DATE_FORMAT)

It is throwing error like

Exception in thread "main" java.time.format.DateTimeParseException: Text '2016-06-06' could not be parsed: Unable to obtain OffsetDateTime from TemporalAccessor: {},ISO resolved to 2016-06-06T00:00 of type java.time.format.Parsed

Can anyone help me how to solve this?

Answer

user7605325 picture user7605325 · Aug 10, 2017

To create an OffsetDateTime, you need the date (day, month and year), the time (hour, minute, second and nanosecond) and the offset (the difference from UTC).

Your input has only the date, so you'll have to build the rest, or assume default values for them.

To parse both formats (yyyy-MM-dd and yyyy/MM/dd), you can use a DateTimeFormatter with optional patterns (delimited by []) and parse to a LocalDate (because you have only the date fields):

// parse yyyy-MM-dd or yyyy/MM/dd
DateTimeFormatter parser = DateTimeFormatter.ofPattern("[yyyy-MM-dd][yyyy/MM/dd]");

// parse yyyy-MM-dd
LocalDate dt = LocalDate.parse("2016-06-06", parser);

// or parse yyyy/MM/dd
LocalDate dt = LocalDate.parse("2016/06/06", parser);

You can also use this (a little bit more complicated, but it works the same way):

// year followed by - or /, followed by month, followed by - or /, followed by day
DateTimeFormatter parser = DateTimeFormatter.ofPattern("yyyy[-][/]MM[-][/]dd");

Then you can set the time to build a LocalDateTime:

// set time to midnight
LocalDateTime ldt = dt.atStartOfDay();

// set time to 2:30 PM
LocalDateTime ldt = dt.atTime(14, 30);

Optionally, you can also use parseDefaulting, as already explained in @greg's answer:

// parse yyyy-MM-dd or yyyy/MM/dd
DateTimeFormatter parser = new DateTimeFormatterBuilder().appendPattern("[yyyy-MM-dd][yyyy/MM/dd]")
    // set hour to zero
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    // set minute to zero
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    // create formatter
    .toFormatter();
// parse the LocalDateTime, time will be set to 00:00
LocalDateTime ldt = LocalDateTime.parse("2016-06-06", parser);

Note that I had to set hour and minute to zero. You can also set seconds (ChronoField.SECOND_OF_MINUTE) and nanoseconds (ChronoField.NANO_OF_SECOND) to zero, but setting the hour and minute are enough to set all the other fields to zero.


You told that you want to use the system's default offset. This is a little bit tricky.

The "system's default offset" will depend on the system's default timezone. And a timezone can have more than one offset, depending on when you are in the timeline.

I'll use my system's default timezone (America/Sao_Paulo) as an example. In the code below I'm using ZoneId.systemDefault(), but remember that this will be different in each system/environment. For all the examples below, remember that ZoneId.systemDefault() returns the America/Sao_Paulo timezone. If you want to get a specific one, you should use ZoneId.of("zone_name") - actually this is prefered.

First you must get the list of valid offsets for the LocalDateTime, at the specified timezone:

// using the parser with parseDefaulting
LocalDateTime ldt = LocalDateTime.parse("2016-06-06", parser);

// get all valid offsets for the date/time, in the specified timezone
List<ZoneOffset> validOffsets = ZoneId.systemDefault().getRules().getValidOffsets(ldt);

According to javadoc, the validOffsets list size, for any given local date-time, can be zero, one or two.

For most cases, there will be just one valid offset. In this case, it's straighforward to get the OffsetDateTime:

// most common case: just one valid offset
OffsetDateTime odt = ldt.atOffset(validOffsets.get(0));

The other cases (zero or two valid offsets) occurs usually due to Daylight Saving Time changes (DST).

In São Paulo timezone, DST will start at October 15th 2017: at midnight, clocks shift forward to 1 AM and offset changes from -03:00 to -02:00. This means that all local times from 00:00 to 00:59 don't exist - you can also think that clocks change from 23:59 directly to 01:00.

So, in São Paulo timezone, this date will have no valid offset:

// October 15th 2017 at midnight, DST starts in Sao Paulo
LocalDateTime ldt = LocalDateTime.parse("2017-10-15", parser);
// system's default timezone is America/Sao_Paulo
List<ZoneOffset> validOffsets = ZoneId.systemDefault().getRules().getValidOffsets(ldt);
System.out.println(validOffsets.size()); // zero

There are no valid offsets, so you must decide what to do in cases like this (use a "default" one? throw exception?).
Even if your timezone doesn't have DST today, it could've had in the past (and dates in the past can be in that case), or it can have in the future (because DST and offsets of any country are defined by governments and laws, and there's no guarantee that nobody will change in the future).

If you create a ZonedDateTime, though, the result will be different:

// October 15th 2017 at midnight, DST starts in Sao Paulo
LocalDateTime ldt = LocalDateTime.parse("2017-10-15", parser);
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());

The zdt variable will be 2017-10-15T01:00-02:00[America/Sao_Paulo] - the time and the offset are adjusted automatically to 1 AM at -02:00 offset.


And there are the case of two valid offsets. In São Paulo, DST will end at February 18th 2018: at midnight, clocks will shift 1 hour back to 11 PM of 17th, and the offset changes from -02:00 to -03:00. This means that all local times between 23:00 and 23:59 will exist twice, in both offsets.

As I set the default time to midnight, there will be only one valid offset. But suppose I decided to use a default time at 23:00:

// parse yyyy-MM-dd or yyyy/MM/dd
parser = new DateTimeFormatterBuilder().appendPattern("[yyyy-MM-dd][yyyy/MM/dd]")
    // *** set hour to 11 PM ***
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 23)
    // set minute to zero
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    // create formatter
    .toFormatter();

// February 18th 2018 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 17th exist twice
LocalDateTime ldt = LocalDateTime.parse("2018-02-17", parser);
// system's default timezone is America/Sao_Paulo
List<ZoneOffset> validOffsets = ZoneId.systemDefault().getRules().getValidOffsets(ldt);
System.out.println(validOffsets.size()); // 2

There will be 2 valid offsets for the LocalDateTime. In this case, you must choose one of them:

// DST offset: 2018-02-17T23:00-02:00
OffsetDateTime dst = ldt.atOffset(validOffsets.get(0));

// non-DST offset: 2018-02-17T23:00-03:00
OffsetDateTime nondst = ldt.atOffset(validOffsets.get(1));

If you create a ZonedDateTime, though, it'll use the first offset as the default:

// February 18th 2018 at midnight, DST ends in Sao Paulo
LocalDateTime ldt = LocalDateTime.parse("2018-02-17", parser);
// system's default timezone is America/Sao_Paulo
List<ZoneOffset> validOffsets = ZoneId.systemDefault().getRules().getValidOffsets(ldt);

// by default it uses DST offset
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault());

zdt will be 2018-02-17T23:00-02:00[America/Sao_Paulo] - note that it uses the DST offset (-02:00) by default.

If you want the offset after DST ends, you can do:

// get offset after DST ends
ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault()).withLaterOffsetAtOverlap();

zdt will be 2018-02-17T23:00-03:00[America/Sao_Paulo] - it uses the offset -03:00 (after DST ends).


Just reminding that the system's default timezone can be changed even at runtime, and it's better to use a specific zone name (like ZoneId.of("America/Sao_Paulo")). You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds().