Parsing Dates in Java 8

Posted by n3integration on February 9, 2016

For anyone working with raw data, you know that working with dates can be tedious and Java’s SimpleDateFormat class is limited in it’s capabilities. With the release of Java 8, the JDK now comes with a DateTimeFormatter class - with a DateTimeFormatterBuilder companion class - that is much more capable.

To keep things DRY, the DateTimeFormatter class provides a few common date formats built-in:

  • DateTimeFormatter.ISO_INSTANT
  • DateTimeFormatter.ISO_DATE
  • DateTimeFormatter.RFC_1123_DATE_TIME

Of these formats, the ISO_INSTANT format is one of the most common date formats on the world wide web.

Usage
LocalDate date = LocalDate.now();
String text = date.format(DateTimeFormatter.ISO_INSTANT);
LocalDate parsedDate = LocalDate.parse(text, DateTimeFormatter.ISO_INSTANT);

DateTimeFormatterBuilder

As mentioned above, a companion builder class is also provided to help construct complex date formats. After having hand-built date parsers in the past, I truly appreciate how easy it is to construct new date formats using the new DateTime API.

I’m not intentionally trying to pick on any specific vendor or product, but the default date format for Oracle databases is yyyy-mm-dd. This is problematic for an obvious reason - what about time granularity? Although this is the default format returned from Oracle for date data types, one can format the date to the desired granularity when retrieving the date. For these cases, you would most likely want to construct a DateTimeFormatter to leniently support either form.

Example

Let’s step through the construction of a DateTimeFormatter to handle this exact use case.

First, let’s allow flexible case formatting by setting the case insensitive flag within the parser.

final DateTimeFormatter ORACLE_DATE = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()

Then, let’s build out the default date format pattern, using both predefined ChronoField constants and literals.

.appendValue(ChronoField.YEAR)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH)

Since the default date format does not support time granularity, we need to wrap the time string within optionalStart and optionalEnd method calls. This allows the parser to optionally ignore any text after the preceding date format if it does not match the desired format.

.optionalStart()
    .appendLiteral(' ')
    .appendValue(ChronoField.HOUR_OF_DAY)
    .appendLiteral(':')
    .appendValue(ChronoField.MINUTE_OF_HOUR)
    .appendLiteral(':')
    .appendValue(ChronoField.SECOND_OF_MINUTE)
.optionalEnd()

Because the time is optional, we must also provide default values for the hour, minute, and second.

.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)

To better understand the example, the code is included in its entirety below.

final DateTimeFormatter ORACLE_DATE = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .appendValue(ChronoField.YEAR)
    .appendLiteral('-')
    .appendValue(ChronoField.MONTH_OF_YEAR)
    .appendLiteral('-')
    .appendValue(ChronoField.DAY_OF_MONTH)
    .optionalStart()
        .appendLiteral(' ')
        .appendValue(ChronoField.HOUR_OF_DAY)
        .appendLiteral(':')
        .appendValue(ChronoField.MINUTE_OF_HOUR)
        .appendLiteral(':')
        .appendValue(ChronoField.SECOND_OF_MINUTE)
    .optionalEnd()
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
    .toFormatter();