Time parsing issue on Android

2020-03-24 04:08发布

I am getting a parse exception when trying to parse the time string 02:22 p.m..

I have the following conversion function:

public static long convertdatetotimestamp(String datestring, String newdateformat, String olddateformat){
    SimpleDateFormat originalFormat = new SimpleDateFormat(olddateformat,Locale.ROOT);
    SimpleDateFormat targetFormat = new SimpleDateFormat(newdateformat,Locale.ROOT);
    Date date = null;
    try {
        date = originalFormat.parse(datestring);
        String formattedDate = targetFormat.format(date);
        Date parsedDate = targetFormat.parse(formattedDate);
        long nowMilliseconds = parsedDate.getTime();

        return nowMilliseconds;
    } catch (ParseException e) {
        e.printStackTrace();
        return 0;
    }

}

The method is called in another activity with a time format "02:22 p.m.". olddateformat and newdateformat are the same: hh:mm a.

It causes following error in log:

java.text.ParseException: Unparseable date: "02:22 p.m." (at offset 6)

How to resolve this issue? Time is in exactly above mentioned format.

3条回答
倾城 Initia
2楼-- · 2020-03-24 04:44

I believe that SimpleDateFormat can't be customized to parse the p.m. part (it only recognizes AM or PM).

So one alternative is to remove the dots:

String time = "02:22 p.m.";
SimpleDateFormat format = new SimpleDateFormat("hh:mm a", Locale.ROOT);
date = format.parse(time.replaceAll("\\.", ""));

One detail: to get the nowMilliseconds value, you need all the date fields (day/month/year) and a timezone. As those fields are not in the input String, SimpleDateFormat sets them to January 1st of 1970 (and also set the seconds and milliseconds to zero), and use the system's default timezone.

I'm not sure if this behaviour of getting January 1970 is consistent among all Java versions, which is another problem because you can get different values depending on the environment/device the code is running. Actually, you might have a different result anyway because it uses the system's default timezone and this can vary among different environments.

If I run this code in my machine, it uses my system's default timezone (America/Sao_Paulo), and the result is 62520000. But if I change the timezone to another (let's say, Asia/Kolkata), the result is 31920000. You must be aware of this variation and check if that's what you really need.

Another detail is that, if olddateformat and newdateformat are the same, there's no need to create 2 different formatters.


Java's new Date/Time API

The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.

In Android you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. You'll also need the ThreeTenABP (more on how to use it here).

All the relevant classes are in the org.threeten.bp package.

With this new API, you can customize the text that corresponds to AM/PM using a org.threeten.bp.format.DateTimeFormatterBuilder (so no need to remove the dots manually). And there are specific classes to each case - in this case, the input has only the time fields (hour and minutes), so I'm going to use the org.threeten.bp.LocalTime class (which represents only a time - hour/minute/second/nanosecond - without a date):

String time = "02:22 p.m.";
// map AM and PM values to strings "a.m." and "p.m."
Map<Long, String> map = new HashMap<Long, String>();
map.put(0L, "a.m.");
map.put(1L, "p.m.");
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // hour and minute
    .appendPattern("hh:mm ")
    // use custom values for AM/PM
    .appendText(ChronoField.AMPM_OF_DAY, map)
    // create formatter
    .toFormatter(Locale.ROOT);

// parse the time
LocalTime parsedTime = LocalTime.parse(time, fmt);

The parsedTime variable will contain the values corresponding to 02:22 PM (and only this value, it has no date fields (day/month/year) nor a timezone).

To get the milliseconds value (number of milliseconds since 1970-01-01T00:00Z), you also need a date (day/month/year) and a timezone. As I said previously, those fields can affect the final value.

In the old API, SimpleDateFormat tries to be "smart" and sets default values for those fields (January 1st of 1970 in the system's default timezone), but the new API is more strict about that and you must tell explicity what date and timezone you want.

In this example, I'm using the Asia/Kolkata timezone but you can change it according to your needs (more on that below):

import org.threeten.bp.LocalDate;
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;

// timezone for Asia/Kolkata
ZoneId zone = ZoneId.of("Asia/Kolkata");
// current date in Kolkata timezone
LocalDate now = LocalDate.now(zone);
// get the parsed time at the specified date, at the specified zone
ZonedDateTime zdt = parsedTime.atDate(now).atZone(zone);
// get the millis value
long millis = zdt.toInstant().toEpochMilli();

If you want a specific date instead of the current date, you can use LocalDate.of(2017, 5, 20) - this will get May 20th, 2017, for example. With this, you can set the code above to the date and timezone you need.

Note that the API uses IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Asia/Kolkata). Avoid using the 3-letter abbreviations (like IST or PST) because they are ambiguous and not standard.

You can get a list of available timezones (and choose the one that fits best your system) by calling ZoneId.getAvailableZoneIds().


If you want to emulate exactly what SimpleDateFormat does, you can use LocalDate.of(1970, 1, 1) and use the default timezone with ZoneId.systemDefault() - but this is not recommended, because the system's default can be changed without notice, even at runtime. It's better to explicit what timezone you're using.

Or you can create a formatter that always sets default values for the date (using the org.threeten.bp.temporal.ChronoField class) and always uses the same timezone. So you can parse it directly to a org.threeten.bp.Instant and get the millis value:

String time = "02:22 p.m.";
ZoneId zone = ZoneId.of("Asia/Kolkata");
DateTimeFormatter fmt2 = new DateTimeFormatterBuilder()
    // hour and minute
    .appendPattern("hh:mm ")
    // use custom values for AM/PM (use the same map from previous example)
    .appendText(ChronoField.AMPM_OF_DAY, map)
    // default value for day: 1
    .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
    // default value for month: January
    .parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
    // default value for year: 1970
    .parseDefaulting(ChronoField.YEAR, 1970)
    // create formatter at my specific timezone
    .toFormatter(Locale.ROOT).withZone(zone);
// get the millis value
long millis = Instant.from(fmt2.parse(time)).toEpochMilli();
查看更多
做自己的国王
3楼-- · 2020-03-24 04:45

Following changes that i've made works fine for me.

 public static long convertdatetotimestamp(String datestring, String newdateformat, String olddateformat){
        DateFormat originalFormat = new SimpleDateFormat(olddateformat,Locale.ENGLISH);
        DateFormat targetFormat = new
                SimpleDateFormat(newdateformat,Locale.ENGLISH);
        Date date = null;
        try {
            date = originalFormat.parse(datestring.replaceAll("\\.", ""));
            String formattedDate = targetFormat.format(date);
            Date parsedDate = targetFormat.parse(formattedDate);
            long nowMilliseconds = parsedDate.getTime();

            return nowMilliseconds;
        } catch (ParseException e) {
            e.printStackTrace();
            return 0;
        }

    }

Locale.ENGLISH you can use your locale, english solved my issue. Reference.

Thanks for responses and references.

查看更多
手持菜刀,她持情操
4楼-- · 2020-03-24 05:01

It so happens that a.m. and p.m. are called just this in Gaelic locale. At least on my Java 8. I am far from sure that it will be the case on (all) Android phones, but you may do some experiments with it.

String datestring = "02:22 p.m.";
Locale parseLocale = Locale.forLanguageTag("ga");
DateTimeFormatter originalFormat = DateTimeFormatter.ofPattern("hh:mm a", parseLocale);
System.out.println(LocalTime.parse(datestring, originalFormat));

This prints

14:22

As Hugo so warmly and rightly recommends is his answer, I am using the modern Java date and time API, so you will need ThreeTenABP for the above code. See How to use ThreeTenABP in Android Project. Alternatively you may want to try the same locale with your otherwise outdated SimpleDateFormat.

US Spanish locale shows the same behaviour on my Java 8, so you may try that too: Locale.forLanguageTag("es-US").

查看更多
登录 后发表回答