a expects either PM or AM in upper case. To get a case insensitive formatter you need to build it manually:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .appendPattern("EEE MMM dd, yyyy h:mma z")
        .toFormatter(Locale.US);

Note that you will get a new error because the 16th of July is not a Wednesday.

Answer from assylias on Stack Overflow
Top answer
1 of 3
4

You cannot parse the Strings due to two h in your format pattern instead of just one, and due to am-pm of day being in lower case letters. The a in the pattern String expects (standard/default) upper case, like AM.

You can either manipulate the Strings as shown in the other answer(s), but you can also use a DateTimeFormatter that can handle lower-case things. All you need is to build one with a DateTimeFormatterBuilder and make it parseCaseInsensitive() and apply a Locale:

public static void main(String[] args) {
    String dtStr = "1:23am-1:08am";
    String t1 = dtStr.split("-")[0];
    String t2 = dtStr.split("-")[1];
    // build a formatter that parses lower case am-pm of day
    DateTimeFormatter format = new DateTimeFormatterBuilder()
                                    .parseCaseInsensitive() // handles lower- and upper-case 
                                    .appendPattern("h:mma")
                                    .toFormatter(Locale.ENGLISH); // doesn't reliably work without a Locale
    LocalTime time1 = LocalTime.parse(t1, format);
    LocalTime time2 = LocalTime.parse(t2, format);
    Duration dur = Duration.between(time1, time2);
    System.out.println(dur.toMinutes() + " minutes " + dur.toSecondsPart() + " seconds");
}

Output:

-15 minutes 0 seconds

This is a little more flexible than manipulating the input before parsing, because this one can also parse upper-case am-pm of day.

2 of 3
3

You need to use the following pattern h:mma because of one-digit hour, also a parses AM/PM in uppercase

String dtStr = "1:23am-1:08am";
String t1 = dtStr.split("-")[0].toUpperCase();
String t2 = dtStr.split("-")[1].toUpperCase();
DateTimeFormatter format = DateTimeFormatter.ofPattern("h:mma");

LocalTime time1 = LocalTime.parse(t1, format);
LocalTime time2 = LocalTime.parse(t2, format);
Duration dur = Duration.between(time1, time2);
System.out.println(dur.toMinutes() + " minutes " + dur.toSecondsPart() + " seconds");
-15 minutes 0 seconds

java.time format specifications

Top answer
1 of 3
3

Use proper date-time objects for your dates and times

For the vast majority of purposes you should not keep your date and time in a string and should not convert your date and time from a string in one format to a string in another format. Keep your date and time in a ZonedDateTime or LocalDateTime object.

When you are required to accept string input, parse that input into a date-time object immediately. I am using and recommending java.time, the modern Java date and time API:

    DateTimeFormatter inputFormatter = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm:ss");
    String input = "11/06/2020 04:14:20";
    LocalDateTime dateTime = LocalDateTime.parse(input, inputFormatter);
    System.out.println(dateTime);

Output so far is:

2020-06-11T04:14:20

Since there is no AM or PM in your string, I have assumed that 04:14:20 was the time of day from 00:00:00 through 23:59:59. If you intended otherwise, you need to explain how.

Only when you need to give string output, format your date and time back into a string of appropriate format:

    DateTimeFormatter outputFormatter = DateTimeFormatter
            .ofPattern("MMMM dd, yyyy hh:mm:ss a", Locale.ENGLISH);
    String output = dateTime.format(outputFormatter);
    System.out.println(output);

June 11, 2020 04:14:20 AM

Do provide a locale for the formatter so Java knows which language to use for the month name and the AM/PM indicator.

What went wrong in your code?

Your string has no AM nor PM: 11/06/2020 04:14:20. Yet your format pattern string requires an AM/PM marker in the end. This is what format pattern letter a signifies. So your string wasn’t in the format that you required. This was the reason for the exception that you observed.

Link

Oracle tutorial: Date Time explaining how to use java.time.

2 of 3
2

You can check this code you have to pass the am/pm part too with the date string value as your format is expecting that.

//String date = "11/06/2020 04:14:20";
String date = "11/06/2020 04:14:20 am";
DateFormat df = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss a");

https://ideone.com/3nibwJ

Top answer
1 of 2
2

This is a tricky one, because the syntax for an optional section of the DateTimeFormatter class specifically wants the space between the optional element and its predecessor included within square brackets.

The documentation of the parseBest() method shows an example of a pattern with an optional timezone ID, uuuu-MM-dd HH.mm[ VV], where the space between the minute and the timezone ID is included within the optional section.

In your snippet, the first optional was written properly hh:mm:ss.SSS[ a... (with the space between SSS and a included within square brackets), but then the optional part for the timezone name was written a [z]zzz instead of a[ z]zzz. In your case, you can either nest the optional sections like so hh:mm:ss.SSS[[ a] zzz] E MMM dd yyyy, or write them as individual optional sections hh:mm:ss.SSS[ a][ z][ zzz] E MMM dd yyyy.

Here is a re-written version of your code covering the 4 cases:

public class Main {
    public static void main(String[] args) {
        parseDate("09:10:05.584 AM EST Wed Nov 29 2023", TIME_FORMAT);
        parseDate("09:10:05.584 AM Wed Nov 29 2023", TIME_FORMAT);
        parseDate("09:10:05.584 EST Wed Nov 29 2023", TIME_FORMAT);
        parseDate("09:10:05.584 Wed Nov 29 2023", TIME_FORMAT);
    }
    public static final String TIME_FORMAT = "HH:mm:ss.SSS[ a][ z][zzz] E MMM dd yyyy";

    public static String parseDate(String input, String format) {
        String retVal = "";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format, Locale.ENGLISH);
        TemporalAccessor ta = formatter.parseBest(input, LocalDateTime::from, LocalDate::from);
        retVal = ta.toString();
        System.out.println("Parsed date is: " + retVal);
        return retVal;
    }
}

You can quickly check the code here at this link:

https://ideone.com/bpfw8o

2 of 2
0

specifying the format this way:

public static final String TIME_FORMAT = "hh:mm:ss.SSS[ a][ z][ zzz] E MMM dd yyyy";

worked. Trying to nest the [] was not working.

Top answer
1 of 2
3
  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);
2 of 2
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
Find elsewhere
Top answer
1 of 2
3

24-hour time not appropriate for some locales

If localizing for the United States, then you should not be aiming for 24-hour time. 24-hour time is generally not used in the US outside of the military. Indeed, Americans often refer to it as "military time".

So your two statements "how to use 24 hours instead of AM/PM?" and "This is for all different locales" are contradictory. You cannot have it both ways. Either you want to control the output such as forcing 24 hour clock, or you want to localize.

If you really need a specific format, use the Answer by lkatiforis. But if your goal is to localize automatically, then you have to "go with the flow" regarding the results. Some locales will localize to 24-hour clock, and some locales will localize to 12-hour clock.

If you want to know where Java gets its rules for how to localize date-time values (translation, and cultural norms), see the Common Locale Data Repository (CLDR) maintained by the Unicode Consortium. The CLDR is used by default in most implementations of Java 9 and later.

If you want to see the various locales in action, here is some example code.

LocalDateTime ldt = LocalDateTime.of( 2021 , 1 , 23 , 18 , 30 , 45 , 0 );
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime( FormatStyle.SHORT );
for ( Locale locale : Locale.getAvailableLocales() )
{
    String output = ldt.format( f.withLocale( locale ) );
    System.out.println( output + " | " + locale.getDisplayName( Locale.US ) + " | " + locale );
}

When run.

…
23.01.21 18:30 | Latvian | lv
23/01/2021, 18:30 | English (Niue) | en_NU
23/01/21 下午6:30 | Chinese (Simplified, Singapore) | zh_SG_#Hans
2021-01-23 6:30 𞤇𞤎 | Fulah (Adlam, Liberia) | ff_LR_#Adlm
23/01/2021, 6:30 pm | English (Jamaica) | en_JM
23/01 2021 18:30 | Kako | kkj
2021-01-23 18:30 | Northern Sami (Latin, Norway) | se_NO_#Latn
2021-01-23 6:30 منجهند، شام | Sindhi (Arabic) | sd__#Arab
23/1/21 18:30 | Spanish (Bolivia) | es_BO
2021-01-23 ཆུ་ཚོད་ 6 སྐར་མ་ 30 ཕྱི་ཆ་ | Dzongkha (Bhutan) | dz_BT
23/1/21, 6:30 পি এম | Manipuri | mni
…
2 of 2
2

You should first check if the date string matches any of the Predefined Formatters.

If not, then you have to make your own Formatter using .ofPattern(String pattern) or .ofPattern(String pattern, Locale locale):

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yy HH:mm:ss", Locale.US);

System.out.println(formatter.format(LocalDateTime.now()));

Output:

11/13/21 19:41:43

Top answer
1 of 3
1

I'm having the same problem. I found this: https://bugs.openjdk.org/browse/JDK-8297504

Basically it says in older unicode CLDR (v33) which java 11 is based on, German terms for AM/PM is nachm. and vorm.

But in CLDR v44 which java17 is based on they changed it to AM/PM.

This is the quote from the ticket(https://bugs.openjdk.org/browse/JDK-8297504):

It turned out that the change came from the upstream CLDR. In CLDR v33 which JDK 11 is based on, "pm" string is localized into German "nachm." (look for "·de·" in the right column): https://www.unicode.org/cldr/cldr-aux/charts/33/by_type/date_&_time.gregorian.html#72c7f54616968b69

whereas in CLDR v42 which the latest JDK is based on, "pm" string in German is "PM": https://unicode-org.github.io/cldr-staging/charts/latest/by_type/date_&_time.gregorian.html#72c7f54616968b69

So the current right way in German based on recent unicode is AM/PM

2 of 3
0

Creating proper datetime format with correct Chronology and Locale will give am/pm in desired locale. For Germany and Chinese, I was able to get correct data, but for Hindi and Italy it’s not giving expected output. May be this is how data is showed in their respective region.

final String pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.FULL, FormatStyle.FULL, Chronology.ofLocale(Locale.GERMAN),Locale.GERMAN);
final DateTimeFormatter targetFormat = DateTimeFormatter.ofPattern(pattern).withLocale(Locale.GERMAN);
final String value = ZonedDateTime.now().format(targetFormat);
🌐
How to do in Java
howtodoinjava.com › home › java date time › format timestamp in 12 hours pattern (am-pm) in java
Format Timestamp in 12 Hours Pattern (AM-PM) in Java
February 18, 2022 - import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class Main { public static void main(String[] args) { String pattern = "hh:mm:ss a"; //1. LocalTime LocalTime now = LocalTime.now(); System.out.println(now.format(DateTimeFormatter.ofPattern(pattern))); //2.
Top answer
1 of 4
7

Two separate problems:

Wrong counts

You're using e.g. MM which is an explicit: Always the full amount of digits, zero-padded. Your string is not like that, it's just the number. So, make that M/d/uuuu h:m:s a.

EDIT: Changed yyyy to uuuu, thanks, @deHaar. Reasoning: yyyy or uuuu rarely matters, but note that this mean 4 digits are required. The difference kicks in for years before 0: uuuu goes negative, yyyy does not and expects you to use e.g. GG so that you get 44 BC instead of -44. In that way, uuuu is just more correct, even though usually the difference is not going to come up.

Missing locale

The second problem is that you should pretty much never use this version of ofPattern - it has a bug, which you can't catch with unit tests, which makes that a bug that is thousands of times 'heavier', and thus, a real problem.

You need to specify locale. Without it, 'AM' is not going to parse unless your platform default locale is english.

Putting it together

LocalDateTime date = LocalDateTime.parse("1/1/2020 3:4:7 AM",
  DateTimeFormatter.ofPattern("M/d/uuuu h:m:s a", Locale.ENGLISH));

works great.

2 of 4
4

In your snippet:

LocalDateTime
    .parse("1/1/2020 3:4:7 AM", DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss a"));
  • 1 - does not match MM
  • 1 - does not match dd
  • 3 - does not match hh
  • 4 - does not match mm
  • 7 - does not match ss

i.e. the lengths of the formatter pattern parts (e.g. MM) and their respective parts from the string text (e.g. 1) do not match.

You can match them in a few ways, e.g. you can either change the string text to match the formatter pattern or other way around.

You can try this instead:

LocalDateTime
    .parse("01/01/2020 03:04:07 AM", DateTimeFormatter.ofPattern("MM/dd/yyyy hh:mm:ss a"));

Additionally, have a look at the Pattern Letters and Symbols.

🌐
Oracle
docs.oracle.com › javase › 8 › docs › api › java › time › format › DateTimeFormatter.html
DateTimeFormatter (Java Platform SE 8 )
October 20, 2025 - Symbol Meaning Presentation Examples ... text Tue; Tuesday; T e/c localized day-of-week number/text 2; 02; Tue; Tuesday; T F week-of-month number 3 a am-pm-of-day text PM h clock-hour-of-am-pm (1-12) number 12 K hour-of-am-pm (0-11) number 0 k clock-hour-of-am-pm (1-24) number ...
🌐
Baeldung
baeldung.com › home › java › java dates › guide to datetimeformatter
Guide to DateTimeFormatter | Baeldung
March 26, 2025 - Learn how to use the Java 8 DateTimeFormatter class to format and parse dates and times
Top answer
1 of 1
4

You can use DateTimeFormatterBuilder.appendText() method that takes a map with custom values.

According to javadoc, the field AMPM_OF_DAY can have the value 0 for AM and 1 for PM, so you just create a map with these values and pass it to the formatter:

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.LocalTime;

// create map with custom values (0=AM, 1=PM, mapping to values "A" and "P")
Map<Long, String> map = new HashMap<>();
map.put(0L, "A"); // AM mapped to "A"
map.put(1L, "P"); // PM mapped to "P"

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    // hour/minute/second (change to whatever pattern you need)
    .appendPattern("hh:mm:ss ")
    // use the mapped values
    .appendText(ChronoField.AMPM_OF_DAY, map)
    // create formatter
    .toFormatter();

// testing
System.out.println(formatter.format(LocalTime.of(10, 30, 45))); // 10:30:45 A
System.out.println(formatter.format(LocalTime.of(22, 30, 45))); // 10:30:45 P

The output will be:

10:30:45 A
10:30:45 P

And it also works to parse a String:

System.out.println(LocalTime.parse("10:20:30 A", formatter)); // 10:20:30
System.out.println(LocalTime.parse("10:20:30 P", formatter)); // 22:20:30

The output will be:

10:20:30
22:20:30


If you don't control the pattern, you can look for a letter a (the pattern corresponding to the AM/PM field) and replace it by the custom version.

One alternative is to split the format String, using a as the delimiter, then you don't lose the rest of the pattern:

// format with day/month/year hour:minute:second am/pm timezone
String format = "dd/MM/yyyy hh:mm:ss a z";

// split the format ("a" is the AM/PM field)
String[] formats = format.split("a");

DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
    // first part of the pattern (everything before "a")
    .appendPattern(formats[0])
    // use the AM/PM mapped values
    .appendText(ChronoField.AMPM_OF_DAY, map);

// if there's something after the "a", add it
if (formats.length > 1) {
    builder.appendPattern(formats[1]);
}

// create formatter
DateTimeFormatter formatter = builder.toFormatter();

//test
ZonedDateTime z = ZonedDateTime.of(2017, 5, 1, 10, 20, 30, 0, ZoneId.of("America/Sao_Paulo"));
// AM replaced by "A"
System.out.println(formatter.format(z)); // 01/05/2017 10:20:30 A BRT

This algorithm also works if the a is in the beginning or end, or even if there's no a (in this case, I'm adding in the end, I'm not sure if it must be added if not present).

// "a" in the end
String format = "hh:mm:ss a";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // 03:30:00 P

// "a" in the beginning
String format = "a hh:mm:ss";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // P 03:30:00

// no "a" (field is added in the end)
String format = "HH:mm:ss";
DateTimeFormatter formatter = // use same code above
System.out.println(formatter.format(LocalTime.of(15, 30))); // 03:30:00P

You can make some improvements, like if the format doesn't contain a, then don't add it (or add with a space before it), or whatever you want.


PS: this code doesn't handle literals (text inside ') like "dd/MM/yyyy 'at' hh:mm:ss a" (the "at" is a literal and shouldn't be removed/replaced).

I'm not a regex expert (so I'm not 100% sure if it works for all cases), but you can do something like this:

String[] formats = format.split("(?<!\'[^\']{0,20})a|a(?![^\']{0,20}\')");

This regex will ignore a inside quoted text. I have to use {0,20} quantifier because lookahead and lookbehinds (the (? patterns) don't accept *, so this will work if the quoted text before or after the "a" has at most 20 characters (just change this value to what you think it fits best your use cases).

Some tests:

String format = "\'Time: at\' hh:mm:ss a";
DateTimeFormatter formatter = // use same code above, but with the modified regex split
System.out.println(formatter.format(LocalTime.of(15, 30))); // Time: at 03:30:00 P

String format = "dd/MM/yyyy \'at\' hh:mm:ss a z";
DateTimeFormatter formatter = // use same code above, but with the modified regex split
ZonedDateTime z = ZonedDateTime.of(2017, 5, 1, 10, 20, 30, 0, ZoneId.of("America/Sao_Paulo"));
System.out.println(formatter.format(z)); // 01/05/2017 at 10:20:30 A BRT