6

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?

Tom
  • 16,842
  • 17
  • 45
  • 54
dotwin
  • 1,302
  • 2
  • 11
  • 31
  • 1
    If the problem was just thread-safety, why wasn't the solution protecting access to the format with `synchronized` blocks? – BadZen Nov 11 '16 at 23:49
  • Yes, the original problem was really just thread-safe; but now I spent a severe amount of time and effort in moving things over and I would actually just like it to work... I cannot believe that setting general parsing defaults is impossible in `java.time`. – dotwin Nov 11 '16 at 23:52
  • Sounds like a sunk cost fallacy at this point, honestly... You can of course apply a zone to your local time, and then measure the millis since epoch: `ZonedDateTime zdt = local.atZone(ZoneId.of("America/New_York"));` But it seems like you're adding complexity for no reason. – BadZen Nov 11 '16 at 23:57
  • Is there really no way to define general defaults _or_ test weather a `ChronoField` is defined in `pattern`? – dotwin Nov 12 '16 at 01:26
  • 1
    Please tell us the concrete patterns which you cannot use with DTF until now. – Meno Hochschild Nov 12 '16 at 09:41
  • You're asking for increasingly convoluted ways to use the wrong tool for the job by smashing it violently. But you can do that with `getResolverFields()` – BadZen Nov 12 '16 at 14:01
  • @MenoHochschild: E.g., `MM/dd` or `mm:ss` cannot be resolved correctly yet (`07/13` gets resolved to `07/01`). @BadZen: I tried that already; `getResolverFields()` does not work. `DateTimeFormatter.ofPattern("yyyy.MM").getResolverFields()` returns `null` e.g. (JavaDoc states "By default, a formatter has no resolver fields, and thus returns null.") Re "wrong tool for the job": I actually just want to define general defaults. Not more, not less. `java.time` should be able to do that IMO. – dotwin Nov 12 '16 at 14:16

2 Answers2

4

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.

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • I just tested a lot of time strings, and still found some patterns which do not work. E.g., I implemented the defaults exactly like you, but "2015.11" parsed as "yyyy.MM" throws `Exception in thread "main" java.time.format.DateTimeParseException: Text '2015.11' could not be parsed: Conflict found: Year 1970 differs from Year 2015` – dotwin Nov 16 '16 at 07:05
  • 1
    @dotwin Please see my update using YEAR_OF_ERA instead of YEAR. – Meno Hochschild Nov 16 '16 at 07:59
  • Works like a charm! Thank you. – dotwin Nov 16 '16 at 18:15
  • @Meno_Hochschild Please allow me to ask a very quick follow-up question fully related to your answer: this solution using `parseDefaulting` seems to not combine with `appendValueReduced(ChronoField.YEAR_OF_ERA, 2, 2, 1920)`. Do you by any chance know a solution to that? Thanks a lot in advance. – dotwin Nov 30 '16 at 03:09
  • @dotwin I cannot reproduce your specific problem with `appendValueReduced(...)`. Suggesting you to open a new questiion and to describe in detail your code (complete and compilable, but not longer than necessary), your input and the expected output and real output (or exception) because this new question is out of scope of just a comment. – Meno Hochschild Nov 30 '16 at 11:18
0

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

Michael Gantman
  • 7,315
  • 2
  • 19
  • 36