2

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.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • The only reference of `AMPM_OF_DAY` I see in this doc says you use `appendText` instead. https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatterBuilder.html appendText(ChronoField.AMPM_OF_DAY, TextStyle.SHORT) – Omar Abdel Bari Aug 16 '21 at 23:41
  • 1
    Does this answer your question? [Java8 DateTimeFormatter am/pm](https://stackoverflow.com/questions/38250379/java8-datetimeformatter-am-pm) – Dequog Aug 16 '21 at 23:41
  • 1
    The 2nd `optionalStart()` should most likely be an `optionalEnd()`. Then, we are missing a blank between the minutes (seconds) and the `am/pm`-part. Then, `AMPM_OF_DAY` must be appended as `text`. Finally, we cannot take the hour of day as-is, but must take the `CLOCK_HOUR_OF_AMPM`. – Turing85 Aug 16 '21 at 23:48
  • 1
    @BasilBourque I disagree with the duplicate assesment. The problem in the question has nothing to do with the suggested answer. The problem is that the OP is using `HOUR_OF_DAY` instead of `CLOCK_HOUR_OF_AMPM`. If he changes that, uses `.appendLiteral(" ").appendText(AMPM_OF_DAY)` the problem is solved using the builder. The suggested answer is not about the builder. – Edwin Dalorzo Aug 17 '21 at 00:36

2 Answers2

2
  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);
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
0

I believe you need to change HOUR_OF_DAY which goes from (0-23) to CLOCK_HOUR_OF_AMPM which goes from (1-12). I also added a space literal before AM/PM and used appendText instead of appendValue for the AM/PM field.

The following works just fine.

var formatter = new DateTimeFormatterBuilder()
                .appendValue(CLOCK_HOUR_OF_AMPM, 2)
                .appendLiteral(':')
                .appendValue(MINUTE_OF_HOUR, 2)
                .optionalStart()
                .appendLiteral(":")
                .appendValue(SECOND_OF_MINUTE, 2)
                .optionalStart()
                .appendLiteral(" ")
                .appendText(AMPM_OF_DAY)
                .toFormatter();

        //formatter = new DateTimeFormatterBuilder().appendPattern("hh:mm:ss a").toFormatter();
var time = LocalTime.parse("07:20:54 AM", formatted);
System.out.println(time);  // 19:20:54
Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205