Search code examples
javadatedatetime-parsingdatetimeformatter

DateTimeFormatterBuilder with am/pm


I'm trying to build a date time format that accepts AM and PM stamps. For example, whenever the LocalTime.parse("12:00 am") in java is called, with the default format, an exception is thrown. So I'm adding my own format like this

String time = "2:00 am"

DateTimeFormatter format = new DateTimeFormatterBuilder()
                        .appendValue(HOUR_OF_DAY, 2)
                        .appendLiteral(':')
                        .appendValue(MINUTE_OF_HOUR, 2)
                        .optionalStart()
                        .appendLiteral(':')
                        .appendValue(SECOND_OF_MINUTE, 2)
                        .optionalStart()
                        .appendValue(AMPM_OF_DAY)
                        .toFormatter();

LocalTime.parse(time, format);


How ever it seems that it doesn't work. I get:

Exception in thread "main" java.time.format.DateTimeParseException: Text '2:00 am' could not be parsed at index 0

How can I build a correct DateTimeFormatterBuilder that parses the following strings: "2:00 am", "12:15 pm"? And yes I'm aware that you can append formats, but in this case it's needed to use appending values.


Solution

    1. Since hour within AM or PM can be 1 digit, specify so. Use the overloaded appendValue method that accepts a minimum and a maximum field width.
    2. As Edwin Daloezo already said, you probably want ChronoField.CLOCK_HOUR_OF_AMPM.
    3. You need am or pm unconditionally, so it should be outside of optionalStart()optionalEnd() as Turing85 said in a comment.
    4. Consider whether you need to accept am or AM or both. In that last case you need to specify case insensitive parsing.
    5. Always specify a locale (a language) for your formatters. While AM and PM are hardly used in other langauges than English, they do have different values (texts) in other languages.

    So my go is:

        String time = "2:00 am";
    
        DateTimeFormatter format = new DateTimeFormatterBuilder()
                .appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 1, 2, SignStyle.NEVER)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
                .optionalStart()
                .appendLiteral(':')
                .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
                .optionalEnd()
                .appendLiteral(' ')
                .appendText(ChronoField.AMPM_OF_DAY)
                .toFormatter(Locale.forLanguageTag("en-AU"));
    
        System.out.println(LocalTime.parse(time, format));
    

    Output is:

    02:00

    In Australian English am and pm are in lower case (according to my Java 11), so I specified this locale. Which you should only do if your input comes from Australia, or it will just confuse (there are a few more locales where am and pm are in lower case). To accept lower case am and pm from another English-speaking locale, use parseCaseInsensitive(). For example:

        DateTimeFormatter format = new DateTimeFormatterBuilder()
                .appendValue(ChronoField.CLOCK_HOUR_OF_AMPM, 1, 2, SignStyle.NEVER)
                .appendLiteral(':')
                .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
                .optionalStart()
                .appendLiteral(':')
                .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
                .optionalEnd()
                .appendLiteral(' ')
                .parseCaseInsensitive() // Accept AM or am
                .appendText(ChronoField.AMPM_OF_DAY)
                .toFormatter(Locale.ENGLISH);
    

    If you want a format pattern string

    This is no recommendation per se. If you do not need case insensitive parsing, it is possible to build your formatter from a format pattern string rather than a builder. On one hand it may be even more error-prone, on the other it’s much shorter.

        DateTimeFormatter format = DateTimeFormatter
                .ofPattern("h:mm[.ss] a", Locale.forLanguageTag("en-AU"));
    

    Output is the same as before. The square brackets in the format pattern string specify that the seconds are optional.

    If you do need case insensitive parsing, you do need the builder for specifying it.

    You may also mix the approaches since a builder too accepts a format pattern string:

        DateTimeFormatter format = new DateTimeFormatterBuilder()
                .parseCaseInsensitive() // Accept AM or am
                .appendPattern("h:mm[.ss] a")
                .toFormatter(Locale.ENGLISH);