Java 8 Date equivalent to Joda's DateTimeFormatterBuilder with multiple parser formats?
Asked Answered
B

5

30

I currently have a Joda date parser that uses the DateTimeFormatterBuilder with half a dozen different date formats that I may receive.

I'm migrating to Java 8's Date routines and don't see an equivalent.

How can I do something like this using Java 8 Dates?

DateTimeParser[] parsers = { 
    DateTimeFormat.forPattern( "yyyy/MM/dd HH:mm:ss.SSSSSS" ).getParser() ,
    DateTimeFormat.forPattern( "yyyy-MM-dd HH:mm:ss" ).getParser() ,
    DateTimeFormat.forPattern( "ddMMMyyyy:HH:mm:ss.SSS Z" ).getParser() ,
    DateTimeFormat.forPattern( "ddMMMyyyy:HH:mm:ss.SSS" ).getParser() ,
    DateTimeFormat.forPattern( "ddMMMyyyy:HH:mm:ss.SSSSSS" ).getParser() ,
    DateTimeFormat.forPattern( "yyyy-MM-dd HH:mm:ss.SSS" ).getParser() 
};

DateTimeFormatter dateTimeFormatterInput = new DateTimeFormatterBuilder()
     .append( null, parsers ).toFormatter();
Belford answered 23/3, 2016 at 20:49 Comment(1)
You will probably observe a serious decline performance-wise - especially if you plan to implement a replacement by exception handling. According to my own tests, Joda-Time is quicker. See also this JDK-issue.Lifeboat
M
42

There is no direct facility to do this, but you can use optional sections. Optional sections are enclosed inside squared brackets []. This allows for the whole section of the String to parse to be missing.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern(""
    + "[yyyy/MM/dd HH:mm:ss.SSSSSS]"
    + "[yyyy-MM-dd HH:mm:ss[.SSS]]"
    + "[ddMMMyyyy:HH:mm:ss.SSS[ Z]]"
);

This formatter defines 3 grand optional sections for the three main patterns you have. Each of them is inside its own optional section.

Working demo code:

public static void main(String[] args) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(""
        + "[yyyy/MM/dd HH:mm:ss.SSSSSS]"
        + "[yyyy-MM-dd HH:mm:ss[.SSS]]"
        + "[ddMMMyyyy:HH:mm:ss.SSS[ Z]]"
    , Locale.ENGLISH);
    System.out.println(LocalDateTime.parse("2016/03/23 22:00:00.256145", formatter));
    System.out.println(LocalDateTime.parse("2016-03-23 22:00:00", formatter));
    System.out.println(LocalDateTime.parse("2016-03-23 22:00:00.123", formatter));
    System.out.println(LocalDateTime.parse("23Mar2016:22:00:00.123", formatter));
    System.out.println(LocalDateTime.parse("23Mar2016:22:00:00.123 -0800", formatter));
}
Muggins answered 23/3, 2016 at 21:4 Comment(5)
If the locale varies, too, then this code will not work.Lifeboat
Note that you can't use this for formatting, only parsing. When formatting using such a formatter you will get all formats. I'm guessing if you want a few optional formats and one "default" for display you have to use two different DateTimeFormatter instances - one with the optional formats used for parsing, and one with only the default format for displaying.Roswell
Patterns for Formatting and ParsingThermotherapy
Unfortunately .appendPattern("yyyyMMdd'T'HHmmssZ").toFormatter(); does not work but "[yyyyMMdd'T'HHmmssZ]" doesThermotherapy
I should be noted that ordering of the patterns is important, they should be longest to shortest and most strict to least strict. This will not work : DateTimeFormatter formatter = DateTimeFormatter.ofPattern("" + "[yyyy-MM-dd]" + "[yyyy-MM-dd HH:mm:ss]" + "[dd/MM/yyyy]" + "[dd/MM/yyyy HH:mm:ss]") This is correct : DateTimeFormatter.ofPattern("" + "[yyyy-MM-dd HH:mm:ss]" + "[dd/MM/yyyy HH:mm:ss]" + "[yyyy-MM-dd]" + "[dd/MM/yyyy]" )Lacework
A
10

As an alternative answer to Tunaki, you can also use DateTimeFormatterBuilder:

DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder()
  .appendPattern("[yyyy]")
  .appendPattern("[M/d/yyyy]")
  .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
  .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
  .toFormatter()
Alexiaalexin answered 3/11, 2016 at 15:38 Comment(2)
I did not get this to work with more than one append. I created a list of DateTimeFormaters and a loop with try/catch.Islek
@Islek try-catch should be used to handle exceptions and not for expected input. But you are right the proposed code isn't correct. One can use multiple formatters only with appendOptional-method of DateTimeFormatterBuilder.Rand
B
7

Iterating over @Tunaki's solution, using streams, when the code need to accept different patterns in a configurable way :

DateTimeFormatter dateTimeFormatter = dateFormats.stream()
        .map(DateTimeFormatter::ofPattern)
        .reduce(new DateTimeFormatterBuilder(), 
                DateTimeFormatterBuilder::appendOptional, 
                (f1, f2) -> f1.append(f2.toFormatter()))
        .toFormatter();

In this case I don't care about the combiner part of the reducer, but I need it in the signature so I made the combiner correct.

This code would virtually equivalent to if the above patterns (yyyy/MM/dd HH:mm:ss.SSSSSS, yyyy-MM-dd HH:mm:ss[.SSS], ddMMMyyyy:HH:mm:ss.SSS[ Z]) would be fed to the stream :

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .appendOptional(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSSSSS")
    .appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.SSS]"
    .appendOptional(DateTimeFormatter.ofPattern("ddMMMyyyy:HH:mm:ss.SSS[ Z]")
    .toFormatter();
Buckie answered 10/8, 2017 at 15:57 Comment(0)
R
1

Based on answer from @Brice I wrote the following method, which worked best for me

private LocalDate parseDate(String date) {
    DateTimeFormatter yearFormat = new DateTimeFormatterBuilder()
            .appendPattern("yyyy")
            .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
            .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
            .toFormatter();
    DateTimeFormatter yearAndMonthFormat = new DateTimeFormatterBuilder()
            .appendPattern("yyyy-MM")
            .parseDefaulting(ChronoField.DAY_OF_MONTH,1)
            .toFormatter();

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendOptional(DateTimeFormatter.ISO_DATE)
            .appendOptional(yearAndMonthFormat)
            .appendOptional(yearFormat)
            .toFormatter();

    LocalDate result = LocalDate.parse(date, formatter);

    return result;
}

Pay attention to the order of optional formatters added to the last one. Reverse order would only work for dates of the type "yyyy". The proposed order allows me to parse all of the following:

  • 2014
  • 2014-10
  • 2014-03-15

Just adding for the case someone would have similar use case.

Rand answered 15/1, 2019 at 13:23 Comment(0)
B
1

Here is what I eventually came up with. It handles three different major formats, each with multiple minor formatting differences (such as allowing either - or / delimiters) as well as handling variable number of microseconds (from 0 to 6):

private static final DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendPattern( "[ddMMMyyyy:HH:mm:ss" )
        .optionalStart()
        .appendFraction( ChronoField.MICRO_OF_SECOND , 1 , 6 , true )
        .optionalEnd()
        .appendPattern( "[ ][Z][X]]" )
        .appendPattern( "[yyyy[-][/]MM[-][/]dd['T'][ ]HH:mm[:][.]ss" )
        .optionalStart()
        .appendFraction( ChronoField.MICRO_OF_SECOND , 1 , 6 , true )
        .optionalEnd()
        .appendPattern( "[Z][X]]" )
        .appendPattern( "[EEE, dd MMM yyyy HH:mm:ss zzz]" )
        .toFormatter();
Belford answered 16/1, 2019 at 20:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.