Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DateTimeFormatter parsing string with optional time part fails if space removed

Following my other question on how to parse date-only strings as LocalDateTime, on attempt to parse string 20120301122133 using pattern yyyyMMdd[HHmmss] I get an error. Strange thing is that parsing 20120301 122133 using pattern yyyyMMdd[ HHmmss] works perfectly.

So this code works fine

LocalDateTime.parse(
     "19940513 230000", 
     new DateTimeFormatterBuilder()
        .appendPattern("yyyyMMdd[ HHmmss]")
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)   
        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
        .toFormatter()
)

And this one fails

LocalDateTime.parse(
    "19940513230000", 
    new DateTimeFormatterBuilder()
        .appendPattern("yyyyMMdd[HHmmss]")
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) 
        .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)         
        .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
        .toFormatter()
)

How should I parse Strings in format yyyyMMdd[HHmmss], i.e. in format yyyyMMddHHmmss with optional time part using the java 8 time API?

The parse pattern is a configurable option and thus is known only at runtime. So I can't e.g. replace the String pattern with hard-coded DateTimeFormatterBuilder invocations.

like image 524
Kamil Roman Avatar asked Mar 19 '18 08:03

Kamil Roman


3 Answers

    System.out.println(LocalDateTime.parse(
            "19940513230000",
            new DateTimeFormatterBuilder()
                .appendPattern("[uuuuMMddHHmmss][uuuuMMdd]")
                .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) 
                .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)         
                .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
                .toFormatter()
        ));

This prints:

1994-05-13T23:00

If instead I try to parse a string with just the date, "19940513", I get

1994-05-13T00:00

It works with yyyy instead of uuuu too. Assuming all your years are in this era (year 1 or later), it doesn’t make any difference which one you use. Generally uuuu will accept a negative year too, 0 meaning 1 BCE, -1 meaning 2 BCE and so forth.

like image 88
Ole V.V. Avatar answered Nov 02 '22 19:11

Ole V.V.


The problem is that the pattern expression "yyyy" does not indicate a fixed four-digit-year but at least 4 digits (or more, so the parser is greedy). But you can do following:

LocalDateTime ldt =
    LocalDateTime.parse(
        "19940513230000",
        new DateTimeFormatterBuilder()
            .appendValue(ChronoField.YEAR, 4)
            .appendPattern("MMdd[HHmmss]")
            .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
            .toFormatter());
System.out.println(ldt); // 1994-05-13T23:00
like image 2
Meno Hochschild Avatar answered Nov 02 '22 18:11

Meno Hochschild


This happens because the year hasn't a fixed value, it is normally limited to 19 digits.

If you create the following formatter (minutes and seconds are not required) and use the toString() method:

new DateTimeFormatterBuilder()
        .appendPattern("yyyyMMdd[ HHmmss]")
        .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
        .toFormatter()
        .toString();

You could see the following:

"Value(YearOfEra,4,19,EXCEEDS_PAD)Value(MonthOfYear,2)Value(DayOfMonth,2)[' 'Value(HourOfDay,2)Value(MinuteOfHour,2)Value(SecondOfMinute,2)]java.time.format.DateTimeFormatterBuilder$DefaultValueParser@32eff876"

Here you can see that YearOfEra has a minimun width of 4 and a maximun of 19.

You can use one of the answers of Meno or Ole.

However if you need to receive the format and the date as parameters, and you want to be able to specify the date format in a simpler way (e.g. yyyyMMdd[HHmmSS] instead [...][...]), you may preprocess one of the to values (either the format of the date).

You can create 'dinamically' the formatter, so every yyyy is interpreted as a 4 digits year only.

A custom format builder could be something like (can be improved):

public static DateTimeFormatter createFixed4DigitYearFormatter(String format) {
    DateTimeFormatterBuilder formatBuilder = new DateTimeFormatterBuilder();
    Arrays.stream(format.split("yyyy", -1))
            .flatMap(cur -> Stream.of("yyyy", cur)).skip(1)
            .filter(str -> !str.isEmpty())
            .forEach(pattern -> {
                if ("yyyy".equals(pattern)) formatBuilder
                        .appendValue(ChronoField.YEAR_OF_ERA, 4);
                else formatBuilder.appendPattern(pattern);
            });
    return formatBuilder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0).toFormatter();
}

This formater split the format by the string "yyyy", then every non "yyyy" is added as a pattern (with appendPattern(..)) and the "yyyy" is added as a value of type YEAR_OF_ERA with fixed 4 digits (with appendValue(..)).

Finally you can use the formatter with multiple formats:

System.out.println(LocalDateTime.parse("19940513230000",
        createFixed4DigitYearFormatter("yyyyMMdd[HHmmss]"))); // 1994-05-13T23:00
System.out.println(LocalDateTime.parse("19940513",
        createFixed4DigitYearFormatter("yyyyMMdd[HHmmss]"))); // 1994-05-13T00:00
System.out.println(LocalDateTime.parse("1994-05-13 23:00:00",
        createFixed4DigitYearFormatter("yyyy-MM-dd[ HH:mm:ss]"))); // 1994-05-13T23:00
System.out.println(LocalDateTime.parse("1994-05-13",
        createFixed4DigitYearFormatter("yyyy-MM-dd[ HH:mm:ss]"))); // 1994-05-13T00:00
like image 2
Jose Da Silva Avatar answered Nov 02 '22 17:11

Jose Da Silva