2

There already exists question Java 8 DateTimeFormatter with optional part, but the answer for it won't work when optional part is only hours and minutes without seconds:

DateTimeFormatter patternWithOptional = new DateTimeFormatterBuilder()
    .appendPattern("M/d/yyyy[ h:mm]")
    .toFormatter();
TemporalAccessor tmp = patternWithOptional.parseBest("4/11/2020 1:20", LocalDateTime::from, LocalDate::from);
System.out.println(tmp);
System.out.println(tmp.getClass().getSimpleName());
// prints 2020-04-11, without time
// class is LocalDate in this case

This is because LocalDateTime.from(TemporalAccessor temporal) uses TemporalQueries.LOCAL_TIME to query the temporal. TemporalQueries.LOCAL_TIME, in turn, requires ChronoField.NANO_OF_DAY to be available from temporal.

System.out.println(patternWithOptional.parse("4/11/2020 1:20"));
// prints:
{HourOfAmPm=1, MinuteOfHour=20},ISO resolved to 2020-04-11

Since HourOfAmPm and MinuteOfHour are available, the simplest way to achieve what I need, that I could come up with:

private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
    .appendPattern("M/d/yyyy[ h:mm]")
    .toFormatter();

public static void main(String[] args) {
    // demo
    System.out.println(parse("3/20/1995"));      // without time
    System.out.println(parse("4/11/2020 1:20")); // with time
}

private static LocalDateTime parse(String s) {
    TemporalAccessor parsed = FORMATTER.parse(s);
    return LocalDateTime.of(
        LocalDate.from(parsed),
        LocalTime.of(
            parsed.isSupported(ChronoField.HOUR_OF_AMPM)
                ? (int) ChronoField.HOUR_OF_AMPM.getFrom(parsed)
                : 0,
            parsed.isSupported(ChronoField.MINUTE_OF_HOUR)
                ? (int) ChronoField.MINUTE_OF_HOUR.getFrom(parsed)
                : 0
        ) // minor note: this part could also be used as a lambda argument for parseBest method.
    );
}

Is there a simpler way to do this without explicit isSupported checks?

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
andrybak
  • 2,129
  • 2
  • 20
  • 40
  • It’s really a duplicate of [Difference between java HH:mm and hh:mm on SimpleDateFormat](https://stackoverflow.com/questions/17341214/difference-between-java-hhmm-and-hhmm-on-simpledateformat) and of [Comparing time is incorrect when picking 12:00](https://stackoverflow.com/questions/50042873/comparing-time-is-incorrect-when-picking-1200), though that may be a bit tricky to see? – Ole V.V. Jun 27 '18 at 09:09
  • Very well researched question, it’s a pleasure. – Ole V.V. Jun 27 '18 at 09:15

1 Answers1

4

In your format pattern string you need to use uppercase H for hour of day:

            .appendPattern("M/d/yyyy[ H:mm]")

With this change your first snippet prints

2020-04-11T01:20
LocalDateTime

Lowercase h is for hour within AM or PM. So your time might have meant 01:20 AM or 01:20 PM, that is, 13:20. Java could not decide and therefore chose not to interpret the time of day and only give you a LocalDate.

If you always want a LocalDateTime, you may also use parseDefaulting:

    DateTimeFormatter patternWithOptional = new DateTimeFormatterBuilder()
            .appendPattern("M/d/yyyy[ H:mm]")
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .toFormatter();
    System.out.println(LocalDateTime.parse("4/11/2020 1:20", patternWithOptional));
    System.out.println(LocalDateTime.parse("4/11/2020", patternWithOptional));

Output:

2020-04-11T01:20
2020-04-11T00:00

On the other hand, if you do want either a LocalDate or a LocalDateTime depending on whether time of day is available in the string, you don’t need a DateTimeFormatterBuilder:

    DateTimeFormatter patternWithOptional = DateTimeFormatter.ofPattern("M/d/yyyy[ H:mm]");
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • Oh, the `HourOfAmPm` in the output should have tipped me off. I had an incling to try parsing something >12, but didn't follow through with it. `H` is indeed what I needed in my case, but the `parseDefaulting` in builder is exactly what my question asked for. – andrybak Jun 27 '18 at 09:52
  • Just to clarify: I needed both to fix the pattern and add defaults. – andrybak Jun 27 '18 at 10:31