dotwin
dotwin

Reputation: 1332

Problems when moving from SimpleDateFormat to DateTimeFormatter

I have been successfully using SimpleDateFormat for the last couple of years. I built a bunch of time utility classes using it.

As I ran into problems with SimpleDateFormat (SDF) not being thread safe, I spent the last couple of days refactoring these utility classes to internally use DateTimeFormatter (DTF) now. Since both classes' time patterns are almost identical, this transition seemed a good idea at the time.

I now have problems obtaining EpochMillis (milliseconds since 1970-01-01T00:00:00Z): While SDF would e.g. interpret 10:30 parsed using HH:mm as 1970-01-01T10:30:00Z, DTF does not do the same. DTF can use 10:30 to parse a LocalTime, but not a ZonedDateTime which is needed to obtain EpochMillis.

I understand that the objects of java.time follow a different philosophy; Date, Time, and Zoned objects are kept separately. However, in order for my utility class to interpret all strings as it did before, I need to be able to define the default parsing for all missing objects dynamically. I tried to use

DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.parseDefaulting(ChronoField.YEAR, 1970);
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
builder.append(DateTimeFormatter.ofPattern(pattern));

but this does not work for all patterns. It seems to only allow defaults for parameters that are not defined in pattern. Is there a way to test which ChronoFields are defined in pattern to then selectively add defaults?

Alternatively, I tried

TemporalAccessor temporal = formatter.parseBest(time,
        ZonedDateTime::from,
        LocalDateTime::from,
        LocalDate::from,
        LocalTime::from,
        YearMonth::from,
        Year::from,
        Month::from);
if ( temporal instanceof ZonedDateTime )
    return (ZonedDateTime)temporal;
if ( temporal instanceof LocalDateTime )
    return ((LocalDateTime)temporal).atZone(formatter.getZone());
if ( temporal instanceof LocalDate )
    return ((LocalDate)temporal).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof LocalTime )
    return ((LocalTime)temporal).atDate(LocalDate.of(1970, 1, 1)).atZone(formatter.getZone());
if ( temporal instanceof YearMonth )
    return ((YearMonth)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Year )
    return ((Year)temporal).atMonth(1).atDay(1).atStartOfDay().atZone(formatter.getZone());
if ( temporal instanceof Month )
    return Year.of(1970).atMonth((Month)temporal).atDay(1).atStartOfDay().atZone(formatter.getZone());

which does not cover all cases either.

What is the best strategy to enable dynamic date / time / date-time / zone-date-time parsing?

Upvotes: 6

Views: 1200

Answers (2)

Meno Hochschild
Meno Hochschild

Reputation: 44061

Java-8-solution:

Change the order of your parsing instructions inside the builder such that the defaulting instructions all happen AFTER the pattern instruction.

For example using this static code (well, your approach will use an instance-based combination of different patterns, not performant at all):

private static final DateTimeFormatter FLEXIBLE_FORMATTER;

static {
    DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
    builder.appendPattern("MM/dd");
    builder.parseDefaulting(ChronoField.YEAR_OF_ERA, 1970);
    builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
    builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
    builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
    builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
    builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
    FLEXIBLE_FORMATTER = builder.toFormatter();
}

Reason:

The method parseDefaulting(...) works in a funny way, namely like an embedded parser. That means, this method will inject a default value for defined field if that field has not been parsed yet. And the later pattern instruction tries to parse the same field (here: MONTH_OF_YEAR for pattern "MM/dd" and input "07/13") but with a possibly different value. If so then the composite parser will abort because it has found ambivalent values for same field and is unable to resolve the conflict (parsed value 7, but default value 1).

The official API contains following notice:

During parsing, the current state of the parse is inspected. If the specified field has no associated value, because it has not been parsed successfully at that point, then the specified value is injected into the parse result. Injection is immediate, thus the field-value pair will be visible to any subsequent elements in the formatter. As such, this method is normally called at the end of the builder.

We should read it as:

Dont't call parseDefaulting(...) before any parsing instruction for the same field.

Side note 1:

Your alternative approach based on parseBest(...) is even worse because

  • it does not cover all combinations with missing minute or only missing year (MonthDay?) etc. The default value solution is more flexible.

  • it is performancewise not worth to be discussed.

Side note 2:

I would rather have made the whole implementation order-insensitive because this detail is like a trap for many users. And it is possible to avoid this trap by choosing a map-based implementation for default values as done in my own time library Time4J where the order of default-value-instructions does not matter at all because injecting default values only happens after all fields have been parsed. Time4J also offers a dedicated answer to "What is the best strategy to enable dynamic date / time / date-time / zone-date-time parsing?" by offering a MultiFormatParser.

UPDATE:

In Java-8: Use ChronoField.YEAR_OF_ERA instead of ChronoField.YEAR because the pattern contains the letter "y" (=year-of-era, not the same as proleptic gregorian year). Otherwise the parse engine will inject the proleptic default year in addition to parsed year-of-era and will find a conflict. A real pitfall. Just yesterday I had fixed a similar pitfall in my time library for the month field which exists in two slightly different variations.

Upvotes: 4

Michael Gantman
Michael Gantman

Reputation: 7792

I have used new java.time package and it takes time getting used to it. But after a learning curve I have to say it is definitely very comprehensive and robust solution probably superseding Joda time library and other previous solutions. I wrote my own utilities for working with parsing Strings to Date. I wrote a summarizing article that explains how I implemented a feature that parsed String of unknown format to Date. It might be helpful. Here is the link to an article: Java 8 java.time package: parsing any string to date

Upvotes: 0

Related Questions