Convert string to date (CEST works fine, GMT+02:00 doesn’t work)



Question

Why is my Android app unable to parse String str1= "Tue Jun 20 15:56:29 CEST 2017"?

I found some similar questions, but none of them helped me.

Sidenotes

In my project I have some Java applications which are running on a computer and some Android applications. They are able to communicate to each other.

In the messages are timestamps. However my Java applications are sending timestamps in a format like String str1= "Tue Jun 20 15:56:29 CEST 2017" and my Android apps like String str2 = "Tue Jun 20 13:40:37 GMT+02:00 2017". To save the message including the time I have to parse the incoming time to a date.

My Android-App

In my Android app I can’t parse the String str1= "Tue Jun 20 15:56:29 CEST 2017" correctly:

java.text.ParseException: Unparseable date: “Tue Jun 20 15:56:29 CEST 2017”

String str2 = "Tue Jun 20 13:40:37 GMT+02:00 2017"is working fine.

Code:

    String str1 = "Tue Jun 20 14:53:08 CEST 2017";
    SimpleDateFormat formatter = new SimpleDateFormat("EE MMM dd HH:mm:ss zzzz yyyy", Locale.US);
    try {
        Date date = formatter.parse(str1);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    // test
    String str2 = "Tue Jun 20 13:40:37 GMT+02:00 2017";
    formatter = new SimpleDateFormat("EE MMM dd HH:mm:ss zzzz yyyy", Locale.US);
    try {
        Date date = formatter.parse(str2);
    } catch (ParseException e) {
        e.printStackTrace();
    }

My Java-App

However, my Java application can parse both strings correctly.

Code:

    String str = "Tue Jun 20 14:53:08 CEST 2017";
    SimpleDateFormat formatter = new SimpleDateFormat("EE MMM dd HH:mm:ss zzzz yyyy", Locale.US);
    try {
        Date date = formatter.parse(str);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    // test
    str = "Tue Jun 20 13:40:37 GMT+02:00 2017";
    formatter = new SimpleDateFormat("EE MMM dd HH:mm:ss zzzz yyyy", Locale.US);
    try {
        Date date = formatter.parse(str);
    } catch (ParseException e) {
        e.printStackTrace();
    }

ThreeTen Solution

String to LocalDateTime

To convert my incoming string I’m using the following code:

    String time = "Mon Jun 26 15:42:51 GMT 2017";
    DateTimeFormatter gmtDateTimeFormatter = DateTimeFormatter.ofPattern("EE MMM dd HH:mm:ss 'GMT' yyyy", Locale.ENGLISH));
    LocalDateTime timestamp = LocalDateTime.parse(time, gmtDateTimeFormatter);

LocalDateTime to String

To convert my LocalDateTime to a string I used this:

    LocalDateTime timestamp = LocalDateTime.now();
    DateTimeFormatter gmtDateTimeFormatter = DateTimeFormatter.ofPattern("EE MMM dd HH:mm:ss 'GMT' yyyy", Locale.ENGLISH));
    String time = gmtDateTimeFormatter.format(timestamp); 

Answer

Maybe there’s a difference on how Android handles the zzzz pattern (probably Java’s implementation handles it better than Android, so it “guesses” the correct timezone in a way that Android doesn’t). I don’t know.

Anyway, may I suggest you to avoid using those old classes? These old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they’re being replaced by the new APIs.

If you’re using Java 8, consider using the new java.time API. It’s easier, less bugged and less error-prone than the old APIs.

If you’re using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8’s new date/time classes. And for Android, there’s the ThreeTenABP (more on how to use it here).

The code below works for both. The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport (or Android’s ThreeTenABP) is org.threeten.bp), but the classes and methods names are the same.

To parse both formats, you can use a DateTimeFormatter with optional sections. That’s because CEST is a timezone short name and GMT+02:00 is an UTC offset, so if you want to parse both with the same formatter, you’ll need to use one optional section for each format.

Another detail is that short names like CET or CEST are ambiguous and not standard. The new API uses IANA timezones names (always in the format Continent/City, like America/Sao_Paulo or Europe/Berlin).

So, you need to choose one timezone that suits your needs. In the example below, I’ve just picked a timezone that’s in CEST (Europe/Berlin), but you can change it according to what you need – you can get a list of all names using ZoneId.getAvailableZoneIds().

As the new API doesn’t resolve CEST (because of its ambiguity), I need to create a set with the prefered timezone in order to correctly parse the input:

// when parsing, if finds ambiguous CET or CEST, it uses Berlin as prefered timezone
Set<ZoneId> set = new HashSet<>();
set.add(ZoneId.of("Europe/Berlin"));

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // your pattern (weekday, month, day, hour/minute/second)
    .appendPattern("EE MMM dd HH:mm:ss ")
    // optional timezone short name (like "CST" or "CEST")
    .optionalStart().appendZoneText(TextStyle.SHORT, set).optionalEnd()
    // optional GMT offset (like "GMT+02:00")
    .optionalStart().appendPattern("OOOO").optionalEnd()
    // year
    .appendPattern(" yyyy")
    // create formatter (using English locale to make sure it parses weekday and month names correctly)
    .toFormatter(Locale.US);

To parse Tue Jun 20 14:53:08 CEST 2017, just use the formatter:

ZonedDateTime z1 = ZonedDateTime.parse("Tue Jun 20 14:53:08 CEST 2017", fmt);
System.out.println(z1);

The output is:

2017-06-20T14:53:08+02:00[Europe/Berlin]

Note that CEST was mapped to Europe/Berlin, according to the set we created.

To parse Tue Jun 20 13:40:37 GMT+02:00 2017, we can use the same formatter. But GMT+02:00 can be in a lot of different regions, so the API can’t map it to a single timezone. To convert it to the correct timezone, I need to use withZoneSameInstant() method:

// parse with UTC offset
ZonedDateTime z2 = ZonedDateTime.parse("Tue Jun 20 13:40:37 GMT+02:00 2017", fmt)
    // convert to Berlin timezone
    .withZoneSameInstant(ZoneId.of("Europe/Berlin"));
System.out.println(z2);

The output is:

2017-06-20T13:40:37+02:00[Europe/Berlin]


PS: the first case (z1) works in Java 8, but in ThreeTen Backport it’s not setting the timezone to Berlin. To fix it, just call .withZoneSameInstant(ZoneId.of("Europe/Berlin")) as we did with z2.


If you still need to use java.util.Date, you can convert from and to the new API.

In java.time, new methods were added to Date class:

// convert ZonedDateTime to Date
Date date = Date.from(z1.toInstant());

// convert back to ZonedDateTime (using Berlin timezone)
ZonedDateTime z = date.toInstant().atZone(ZoneId.of("Europe/Berlin")); 

In ThreeTen backport (and Android), you can use the org.threeten.bp.DateTimeUtils class:

// convert ZonedDateTime to Date
Date date = DateTimeUtils.toDate(z1.toInstant());

// convert back to ZonedDateTime (using Berlin timezone)
ZonedDateTime z = DateTimeUtils.toInstant(date).atZone(ZoneId.of("Europe/Berlin"));


Source: stackoverflow