3

I have some old Java code that attempts to parse a string using SimpleDateFormat. It is frequently throwing a ParseException:

Non-fatal Exception: java.text.ParseException:
Unparseable date: "06:00:00 PM" at java.text.DateFormat.parse(DateFormat.java:400)

I can't see what is going wrong here. Seems simple. Hoping somebody can spot it.

public static String toTimeString(String time, String inTimeFormat, String outTimeFormat) {
    DateFormat df1 = new SimpleDateFormat(inTimeFormat, Locale.getDefault());
    DateFormat df2 = new SimpleDateFormat(outTimeFormat, Locale.getDefault());
    String timeString = "";

    try {
        Date date = df1.parse(time); //<-- exception thrown here
        _12HrTime = df2.format(date);
    } catch (ParseException e) {
        Crashlytics.recordException(e);
    }
    return timeString;
}

Here is how it is being called when it fails:

toTimeString("06:00:00 PM", "hh:mm:ss a", "h:mm a")

Note that the string 06:00:00 PM comes from a server response.

Every time I run this, it works correctly. But Crashlytics reports this error frequently. What am I missing??

6
  • 1
    There is no guarantee that Locale.getDefault() is going to return a value where a maps to PM. If you are literally calling toTimeString() with values like 06:00:00 PM for everyone, use Locale.US or something else that gives you a consistent result worldwide. "Note that the string 06:00:00 PM comes from a server response" -- um, that's messed up IMHO. Commented Oct 7, 2024 at 21:01
  • That makes sense. I'll give that a try. Thank you! Commented Oct 7, 2024 at 21:05
  • 1
    Do you really want to get a time String that contains a date part that is not just unnecessary but even wrong? Parsing a time of day only, the value of the Date would be completed by adding (the value of) the January 1st, 1970. I think you should not use this old Java code and write your own, much better solution/method using java.time, especially the class LocalTime. You can use Locale.ENGLISH for a consistent and predictable format, but Locale.US might do as well. Commented Oct 8, 2024 at 10:51
  • Similar: How do I format am-pm-of-day to be "AM/PM" rather than "a.m./p.m."? Commented Oct 9, 2024 at 19:07
  • Keep your time in a LocalTime object, not in a string. When reading text input, parse into a LocalTime as the first thing. Only when you need to give string output, format back into a string. Commented Oct 22, 2024 at 18:28

3 Answers 3

3

tl;dr

Specify Locale to be used in parsing localized text PM.

Use modern java.time classes, not legacy classes. Specifically java.time.LocalTime.

LocalTime
    .parse( 
        "06:00:00 PM" ,
        DateTimeFormatter
            .ofPattern( "hh:mm:ss a" )
            .withLocale( Locale.of( "en" , "US" ) )  // ⬅️ Specify locale to be used in parsing localized text `PM`. 
    )

Avoid legacy classes

You are using terribly flawed legacy classes that were years ago supplanted by the modern java.time classes defined in JSR 310.

Appropriate class: LocalTime

And you are attempting to represent a time-of-day value with a class that represents a date with time-of-day — square peg in a round hole. Instead use the appropriate class: LocalTime.

Locale

Specify a Locale to determine the human language and cultural norms to be used in parsing the localized PM. Some cultures use lowercase, pm, some use punctuation, p.m.. And some use entirely different text.

If you omit the locale, the JVM’s current default locale will be used implicitly. That particular locale may not be able to make sense of your text PM.

Example code

String input = "06:00:00 PM" ;
Locale locale = Locale.of( "en" , "US" ) ;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "hh:mm:ss a" ).withLocale( locale ) ;
LocalTime time = LocalTime.parse( input , f ) ;

See an older version of this code run at Ideone.com.

time.toString() = 18:00

Android

Android 26+ bundles an implementation of the java.time classes. For earlier Android, the latest tooling provides most of the functionality via “API desugaring”.

ISO 8601

Tip: Avoid using localized text when exchanging/storing date-time values. Use only ISO 8601 standard values.

For a time-of-day that would be 24-hour clock, with zero seconds being optional, with leading & trailing padded zeros. So, 18:00 for 6 PM.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for this answer. It is very appreciated. For anyone who stumbles on this, I'd like to provide some clarification on this answer.
I have to say that there is way more information here than is necessary to resolve the issue. Yes, java.util.date is an outdated library. Yes, ISO-8601 defines a standard for handling time stamps across the internet. The root cause here is simply that the PM part of the string is not guaranteed to be recognized when the when using Locale.getDefault(). Whether using java.util.Date or java.time, the solution in this very specific use case is to hand Locale.US to the formatter for whichever library one uses for date handling.
@AndroidDev I added a specific note up top about the passing of a Locale being the core issue. Thank you for the critique.
No problem, @BasilBourque! I just wanted to make that more clear. Not a criticism whatsoever. I appreciated all of the detail you added!
2

Either your server knows the locale of your client, or it does not.

If it does not, and it returns a value of 06:00:00 PM regardless of whether the device's locale is typical for Boston, Beirut, Buenos Aires, or Bucharest, then Locale.getDefault() may not work. AM and PM are English, and the default locale should translate those. Even if you only distribute your app in English-speaking countries, the user might have their default device locale set to a non-English language. So, try using a US Locale.

1 Comment

CANADA, for example, is one Locale that will not parse PM, even having language en (BTW you can also use Locale.ROOT to be a bit more neutral)
1

DateTimeFormatterBuilder#parseCaseInsensitive

I recommend you use a case-insensitive DateTimeFormatter:

static DateTimeFormatter dtf = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .appendPattern("hh:mm:ss a")
            .toFormatter(Locale.ENGLISH);

In March 2014, Java 8 introduced the modern, java.time date-time API which supplanted the error-prone legacy, java.util and java.text date-time API. Any new code should use the java.time API*.

Demo using java.time API:

public class Main {
    static DateTimeFormatter dtf = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .appendPattern("hh:mm:ss a")
            .toFormatter(Locale.ENGLISH);

    public static void main(String[] args) {
        // Test
        Stream.of(
                "06:00:00 PM",
                "06:00:00 pm"
        ).forEach(s -> System.out.println(LocalTime.parse(s, dtf)));
    }
}

Output:

18:00
18:00

Online Demo

Learn more about the modern Date-Time API from Trail: Date Time.


If you are receiving an instance of java.util.Date, convert it tojava.time.Instant, using Date#toInstant and derive other date-time classes of java.time from it as per your requirement.

1 Comment

As others indicate, the pm/PM difference is likely to be a locale difference, so specifying the correct locale for the string from the server may be a more precise solution. Still this answer is a good supplement (+1).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.