8

With Java 8's new date time library, the way to parse strings into dates is to use the DateTimeFormatter. LocalDate, LocalTime and LocalDateTime all have a static parse method that takes in a String and a formatter. A potential gotcha is that if your DateTimeFormat does not contain a time portion (or for DateTime, a date portion) you end up getting a parse error even if your pattern matches.

Example

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-DD");
LocalDateTime dt = LocalDateTime.parse("2016-01-11", formatter);

This will throw a DateTimeParseException (as discussed here) with the message

java.time.format.DateTimeParseException:
Text '2016-01-11' could not be parsed:
Unable to obtain LocalDateTime from TemporalAccessor

This is a somewhat unhelpful error message as the text can be parsed since it matches the pattern. Rather the error has to do with the fact that it cannot create the time portion of LocalDateTime. Refactoring the code to the below works:

LocalDateTime ldt = LocalDate.parse("2016-01-11", formatter).atStartOfDay());

My question is, say you had a generic method like such

 public static LocalDateTime getDate(String s, DateTimeFormatter format) {
  ...
  }

Is there a way to determine which static parse method you would need to call to coerce the string into a LocalDateTime by applying some default logic? e.g. If date only use midnight, if time only use today, etc.

Community
  • 1
  • 1
joecoder
  • 325
  • 4
  • 18
  • You could create a Java object that has a parse String field and a switch field or an enum field that tells your static parse method "use midnight", "use today", etc. Pass an instance of that Java object to your static parse method. – Gilbert Le Blanc Jan 14 '16 at 15:42

3 Answers3

11

To parse using a formatter that someone else has created is more tricky than parsing one you can control (where Tunaki's parseDefaulting() answer is correct.) It can be done however:

public static LocalDateTime getDate(String s, DateTimeFormatter format) {
    TemporalAccessor dt = parser.parseBest(
            str, LocalDateTime::from, LocalDate::from, LocalTime::from, YearMonth::from);

    if (dt instanceof LocalDate) {
        return ((LocalDate) dt).atStartOfDay();
    } else if (dt instanceof LocalTime) {
        return ((LocalTime) dt).atDate(LocalDate.now());
    } else if (dt instanceof YearMonth) {
        return ((YearMonth) dt).atDay(1).atStartOfDay();
    } else {
        return LocalDateTime.from(dt);
    }
}

I haven't tested the code above, but it is what parseBest() method was designed to do.

JodaStephen
  • 60,927
  • 15
  • 95
  • 117
10

I think you have the question in reverse. You don't want to determine if a formatter uses only a date or a date/time. You create a formatter based on what you should parse and on what you intend to store the result to. Obviously, if you create a formatter that does not handle the time part, using it to parse into a LocalDateTime is a misconception.

If you need to parse dates that can arrive with two formats like "yyyy-MM-dd" or "yyyy-MM-dd HH:mm:ss" (with a time part), it is not up to the parsing to determine what it should do but up to the formatter to provide defaults in the case that there is no time.

With Java Time, this is done with optional sections and default values. For example, the pattern "yyyy-MM-dd[ HH:mm:ss]" will be able to parse date String (like "2016-01-11") and date/time String (like "2016-01-11 20:10:10"). If you will store that into a LocalDateTime, you will need to provide default values in case there is no time component. This is done with parseDefaulting(field, value): this will tell the formatter to return the given default value for that chrono field if it was not already set.

The following code creates such a formatter and defaults the time part to midnight.

public static void main(String[] args) {
    DateTimeFormatter formatter = 
            new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd[ HH:mm:ss]")
                                          .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
                                          .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
                                          .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
                                          .toFormatter();

    LocalDateTime dt1 = LocalDateTime.parse("2016-01-11", formatter);
    LocalDateTime dt2 = LocalDateTime.parse("2016-01-11 20:10:10", formatter);
}

This logic could of course be extended to parse time only String and defaulting the date component to the current date.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • Thanks. In this case the onus is on the DateTimeFormatter creator and cannot really be guarded against in the static method with adding additional overhead. – joecoder Jan 14 '16 at 16:38
  • 1
    @joecoder What do you mean "by adding additional overhead"? The nice thing with such a formatter (if you can have one) is that you don't need to add overhead. – Tunaki Jan 14 '16 at 18:31
  • Putting code in the static function as in the other examples to check what kind of dates it supports. Your formatter would work great, but if another user called the function with a non defaulting formatter and you didn't want to throw an exception by applying your own defaults. I would argue here that is unnecessary overhead and throwing an exception would be the correct thing to do. – joecoder Jan 14 '16 at 18:44
3

Related to your main question in the title, the DateTimeFormatter only returns a general TemporalAccessor and not the concrete desired result type. Instead users are expected to indirectly use the result of the parser to feed static from(parsed)-methods on the concrete type. Hence you will find in the documentation of these methods what kind of fields are expected for successful parsing:

LocalDate requires EPOCH_DAY

LocalTime requires NANO_OF_DAY

So it is sufficient to query the raw parsed data for these fields to decide if the formatter is date-like, time-like or a combination.

DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH);
TemporalAccessor tacc = dateFormatter.parse("2015-07-24");
System.out.println("date-like: " + tacc.isSupported(ChronoField.EPOCH_DAY)); // true
System.out.println("time-like: " + tacc.isSupported(ChronoField.NANO_OF_DAY)); // false


DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm", Locale.ENGLISH);
tacc = timeFormatter.parse("17:45");
System.out.println("date-like: " + tacc.isSupported(ChronoField.EPOCH_DAY)); // false
System.out.println("time-like: " + tacc.isSupported(ChronoField.NANO_OF_DAY)); // true

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.ENGLISH);
tacc = dateTimeFormatter.parse("2015-07-24 17:45");
System.out.println("date-like: " + tacc.isSupported(ChronoField.EPOCH_DAY)); // true
System.out.println("time-like: " + tacc.isSupported(ChronoField.NANO_OF_DAY)); // true

But else I have to agree with @Tunaki that using default values for fields is the better idea from a design point of view. To finish the code given above you will also need special exception handling if the input does not match the pattern etc.

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • I would assume the below code would work, but it throws an exception `DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-DD").withResolverFields(ChronoField.HOUR_OF_DAY);` `TemporalAccessor accessor = formatter.parse("2016-01-11");` `LocalDateTime dt;` `if(accessor.isSupported(ChronoField.HOUR_OF_DAY)) {` ` dt = LocalDateTime.from(accessor);` ` } else {` ` dt = LocalDate.from(accessor).atStartOfDay();` `}` `DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {},ISO of type java.time.format.Parsed` – joecoder Jan 14 '16 at 16:19
  • 1
    @joecoder YYYY-MM-DD is not okay (weekbased year and month and day-of-year is not a sensible combination to yield a date). And using `withResolverFields()` is not at all relevant here. – Meno Hochschild Jan 14 '16 at 17:02
  • It's just an example format, but what would you recommend for dates only? – joecoder Jan 14 '16 at 17:08
  • 1
    @joecoder You should follow the answer of Tunaki and use default field values for missing parts. My answer is just a hint and not complete. For making this work you would need exception handling, too, due to the limitations of JSR-310 parse engine. For example, it is not easy without exception to query the raw data if the input does not match the pattern (tested by me, not even `parseUnresolved()` helps here). – Meno Hochschild Jan 14 '16 at 17:20