57

I have Service fetch date string from web and then I want to pare it to Date object. But somehow application crashes. This is my string that I'm parsing: 2015-02-05T05:20:02+00:00

onStartCommand()

String datetime = "2015-02-05T05:20:02+00:00";
Date new_date = stringToDate(datetime);

stringToDate()

private Date stringToDate(String s){
    DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
    try{
        return df.parse(s);
    }catch(ParseException e){
        e.printStackTrace();
    }
    return null;
}

LogCat:

02-06 20:37:02.008: E/AndroidRuntime(28565): FATAL EXCEPTION: main
02-06 20:37:02.008: E/AndroidRuntime(28565): Process: com.dotmav.runescapenotifier, PID: 28565
02-06 20:37:02.008: E/AndroidRuntime(28565): java.lang.RuntimeException: Unable to start service com.dotmav.runescapenotifier.GEService@384655b5 with Intent { cmp=com.dotmav.runescapenotifier/.GEService }: java.lang.IllegalArgumentException: Unknown pattern character 'X'
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2881)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.app.ActivityThread.access$2100(ActivityThread.java:144)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1376)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.os.Handler.dispatchMessage(Handler.java:102)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.os.Looper.loop(Looper.java:135)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.app.ActivityThread.main(ActivityThread.java:5221)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.lang.reflect.Method.invoke(Native Method)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.lang.reflect.Method.invoke(Method.java:372)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
02-06 20:37:02.008: E/AndroidRuntime(28565): Caused by: java.lang.IllegalArgumentException: Unknown pattern character 'X'
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.text.SimpleDateFormat.validatePatternCharacter(SimpleDateFormat.java:314)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.text.SimpleDateFormat.validatePattern(SimpleDateFormat.java:303)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.text.SimpleDateFormat.<init>(SimpleDateFormat.java:356)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at java.text.SimpleDateFormat.<init>(SimpleDateFormat.java:249)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at com.dotmav.runescapenotifier.GEService.stringToDate(GEService.java:68)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at com.dotmav.runescapenotifier.GEService.onStartCommand(GEService.java:44)
02-06 20:37:02.008: E/AndroidRuntime(28565):    at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2864)
02-06 20:37:02.008: E/AndroidRuntime(28565):    ... 9 more

EDIT: onDestroy() set alarm for periodical update...

AlarmManager alarm = (AlarmManager)getSystemService(ALARM_SERVICE);
alarm.set(
    AlarmManager.RTC_WAKEUP,
    System.currentTimeMillis() + (1000 * 60),
    PendingIntent.getService(this, 0, new Intent(this, GEService.class), 0)
);
Matjaž
  • 2,096
  • 3
  • 35
  • 55

13 Answers13

49

The Android version of SimpleDateFormat doesn't support the X pattern so XXX won't work but instead you can use ZZZZZ which does the same and outputs the timezone in format +02:00 (or -02:00 depending on the local timezone).

Mickäel A.
  • 9,012
  • 5
  • 54
  • 71
  • 2
    Doesn't work for me. This `2017-04-01T17:31:00+02:00` is parsable by `yyyy-MM-dd'T'HH:mm:ssXXX` by Java, but not in Android. This `yyyy-MM-dd'T'HH:mm:ssZZZZZ` does never match, neither Android nor Java. – Bevor Aug 13 '17 at 15:20
  • 3
    Note that the Android docs currently claim that `SimpleDateFormat` *does* support `X`. But they're wrong. https://stackoverflow.com/a/46855974/423105 – LarsH Oct 20 '17 at 19:31
41

Remove "XXX" from

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");

and everything would work fine.

Go through the list of symbols that can be used inside a SimpleDateFormat constructor. Although the documentation shows the "XXX" format, this doesn't work on Android and will throw an IllegalArgumentException.

Probably you are looking for "yyyy-MM-dd'T'HH:mm:ss.SSSZ"

Change your code to

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); 

or

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); // if timezone is required
Bencri
  • 1,173
  • 1
  • 7
  • 22
Rohit5k2
  • 17,948
  • 8
  • 45
  • 57
  • 6
    yyyy-MM-dd'T'HH:mm:ss doesn't address the +00:00 time zone section of the date. That is handled with z. – zgc7009 Feb 06 '15 at 19:54
  • Go for "yyyy-MM-dd'T'HH:mm:ss.SSSZ" – Rohit5k2 Feb 06 '15 at 19:54
  • 1
    I have `+00:00` for time zone, so I should use: `ZZZZZ`, right? – Matjaž Feb 06 '15 at 20:00
  • no just Z would do. Go through the link I have posted. – Rohit5k2 Feb 06 '15 at 20:00
  • 7
    In java 7 XXX is allowed in the pattern for ISO 8601 time zone like -08; -0800; -08:00. SeeJava doc http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html – Oriol Terradas Dec 11 '15 at 17:34
  • ".SSSXXX" is at the end of the second to last example at: https://developer.android.com/reference/java/text/SimpleDateFormat.html – lilbyrdie Dec 16 '16 at 22:00
  • 18
    even though android developer document shows "XXX" format, that doesn't work for me, throws an illegalArgumentException – tharinduNA Jan 27 '17 at 09:53
  • @Oriol, Android doesn't always keep up with Java; and in this case, lilbyrdie, even the Android docs are currently wrong! See https://stackoverflow.com/a/46855974/423105 – LarsH Oct 20 '17 at 19:30
36

No one has mentioned about this error occurring on pre-nougat devices so I thought to share my answer and maybe it is helpful for those who reached this thread because of it.

This answer rightly mentions that "X" is supported only for Nougat+ devices. I still see that documentation suggests to use "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" and not sure why they don't make this point explicit.

For me, yyyy-MM-dd'T'HH:mm:ssXXX was working fine until I tried to test it on 6.0 device and it started crashing which led me to research on this topic. Replacing it with yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ has resolved the issue and works on all 5.0+ devices.

Wahib Ul Haq
  • 4,185
  • 3
  • 44
  • 41
11

You are using the wrong date formatter.

Use this instead: DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

I think that android in contrast with Java 7 uses Z (as in Java 6) for timezones and not X. So, use this for your date formats.

Rohit5k2
  • 17,948
  • 8
  • 45
  • 57
Thanos
  • 3,627
  • 3
  • 24
  • 35
  • 6
    There are three pattern letters for offsets. Lowercase `z` is for a "general time zone" like "PST" or various other formats. Uppercase `Z` is for an RFC 822 offset. In Java 7, `X` is the correct character for an ISO-8601 timezone (+/-#### or "Z" for no offset from UTC.) `X` works just as expected in a non-Android unit test but causes an exception my phone running Android 6.0. – spaaarky21 Aug 08 '16 at 23:17
10

Since Z and XXX are different, I've implemented the following workaround:

// This is a workaround from Z to XXX timezone format
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") {

    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        StringBuffer rfcFormat = super.format(date, toAppendTo, pos);
        return rfcFormat.insert(rfcFormat.length() - 2, ":");
    }

    @Override
    public Date parse(String text, ParsePosition pos) {
        if (text.length() > 3) {
            text = text.substring(0, text.length() - 3) + text.substring(text.length() - 2);
        }
        return super.parse(text, pos);
    }
}
Alexander K
  • 2,558
  • 1
  • 16
  • 11
  • Thanks for you idea, I changed it a little and now it support parsing from and to UTC timezone format like `1970-01-01T00:00:00Z`, to make all behaviour exactly same as `yyyy-MM-dd'T'HH:mm:ssXXX`. See it [here](https://stackoverflow.com/a/61358841/2949178) – kxfeng Apr 22 '20 at 06:49
8

Android SimpleDateFormat is different from Java 7 SDK and does not support 'X' to parse ISO 8601. You can use the 'Z' or 'ZZZZZ' styles to format and programatically set the time zone to UTC. Here is a util class:

public class DateUtil {

    public static final String iso8601DatePattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ";
    public static final DateFormat iso8601DateFormat = new SimpleDateFormat(iso8601DatePattern);
    public static final TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");

    static {
        iso8601DateFormat.setTimeZone(utcTimeZone);
    }

    public static String formatAsIso8601(Date date) {

        return iso8601DateFormat.format(date);
    }
}
Narek
  • 106
  • 1
  • 2
3

Simple solution:

Use this yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ

Instead of yyyy-MM-dd'T'HH:mm:ss.SSSXXX

Done.

Hiren Patel
  • 52,124
  • 21
  • 173
  • 151
  • This is simply not true - ZZZZZ doesn't work the same as XXX - they use different ISO standards for parsing time zones. For example - tryinh to parse the following "2020-01-27T15:30Z[Europe/London]" using ZZZZZ will fail and with XXX will succeed – Nati Dykstein Jan 28 '20 at 13:51
3

according to android documentation zone offset with X format is supporting in API level 24+

Letter  Date or Time Component      Supported (API Levels)
X       Time zone                   24+

so we can't use for lower APIs, I found a workaround for this issue:

SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date).let {
    StringBuilder(it).insert(it.length - 2, ":").toString()
}
beigirad
  • 4,986
  • 2
  • 29
  • 52
2

The error is saying that simpleDateFormat does not recognize the character X. If you are looking for milliseconds it is represented with the character S.

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
Adam W
  • 972
  • 9
  • 18
1

Use a SimpleDateFormat to produce a properly formatted String output:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

String formattedNow = simpleDateFormat.format(new Date(System.currentTimeMillis()));

Output : 2018-02-27T07:36:47.686Z

Markus
  • 2,071
  • 4
  • 22
  • 44
suhasini
  • 39
  • 4
1

Based on idea from Alexander K, I optimize it and support parsing from and to UTC timezone format like 1970-01-01T00:00:00Z, to make all behaviour exactly same as yyyy-MM-dd'T'HH:mm:ssXXX.

public class IsoSimpleDateFormatBeforeNougat extends SimpleDateFormat {

    public IsoSimpleDateFormatBeforeNougat() {
        super("yyyy-MM-dd'T'HH:mm:ssZ");
    }

    public IsoSimpleDateFormatBeforeNougat(Locale locale) {
        super("yyyy-MM-dd'T'HH:mm:ssZ", locale);
    }

    public IsoSimpleDateFormatBeforeNougat(DateFormatSymbols formatSymbols) {
        super("yyyy-MM-dd'T'HH:mm:ssZ", formatSymbols);
    }

    @Override
    public Date parse(String text, ParsePosition pos) {
        if (text.endsWith("Z")) {
            return super.parse(text.substring(0, text.length() - 1) + "+0000", pos);
        }
        if (text.length() > 3 && text.substring(text.length() - 3, text.length() - 2).equals(":")) {
            text = text.substring(0, text.length() - 3) + text.substring(text.length() - 2);
        }
        return super.parse(text, pos);
    }

    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        StringBuffer rfcFormat = super.format(date, toAppendTo, pos);
        if (rfcFormat.substring(rfcFormat.length() - 5).equals("+0000")) {
            return rfcFormat.replace(rfcFormat.length() - 5, rfcFormat.length(), "Z");
        }
        return rfcFormat.insert(rfcFormat.length() - 2, ":");
    }
}

Test code:

@Test
public void test() throws ParseException {
    //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
    SimpleDateFormat sdf = new IsoSimpleDateFormatBeforeNougat();
    sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));

    assertEquals("1970-01-01T08:00:00+08:00", sdf.format(new Date(0)));
    assertEquals(0L, sdf.parse("1970-01-01T08:00:00+08:00").getTime());

    sdf.setTimeZone(TimeZone.getTimeZone("GMT+0"));

    assertEquals("1970-01-01T00:00:00Z", sdf.format(new Date(0)));
    assertEquals(0L, sdf.parse("1970-01-01T00:00:00Z").getTime());
}
kxfeng
  • 306
  • 3
  • 6
0

The following class can be used to convert the string to date when pattern doesn't aware of it.

    import java.text.SimpleDateFormat;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;

/**
 * StringToDateFormater is a concrete class for formatting and parsing dates in a locale-sensitive manner. It allows for
 * formatting (date → text), parsing (text → date), and normalization.
 * 
 * This class is mainly used for convert the date from string. It should be used only when date pattern doesn't aware of
 * it. 
 *
 */
public class StringToDateFormater extends SimpleDateFormat {

    private static final List<String> DATE_SUPPORTED_FORMAT_LIST = Arrays.asList("yyyyMMddHHmmss", "yyyyMMddHHmm",
            "yyyyMMddHHmm", "yyyyMMddHH", "yyyyMMdd", "yyyyMMddHHmmssSS");

    /**
     * 
     */
    private static final long serialVersionUID = -1732857502488169225L;

    /**
     * @param pattern
     */
    public StringToDateFormater() {
    }

    @Override
    public Date parse(String source) {
        Date date = null;

        SimpleDateFormat dateFormat = null;
        for (String format : DATE_SUPPORTED_FORMAT_LIST) {
            dateFormat = new SimpleDateFormat(format);
            try {
                return dateFormat.parse(source);
            } catch (Exception exception) {

            }

        }

        return date;
    }
}
Sudhakar
  • 3,104
  • 2
  • 27
  • 36
  • Order is in the list is important, correct? If yes, the last case in this snippet will never match for example, because the previous one will work before. Also there is a duplicate.. – Kikiwa Jul 19 '16 at 08:30
0

tl;dr

long millisecondsSinceEpoch = OffsetDateTime.parse( "2015-02-05T05:20:02+00:00" ).plusHour( 1 ).toInstant().toEpochMilli()  // Warning: Possible loss of data in truncating nanoseconds to milliseconds. But not in this particular case.

Details

Other answers are correct but now outdated. The old date-time classes are now legacy. Use java.time classes instead.

ISO 8601

The input String is in standard ISO 8601 format. Parse directly, no need to define a formatting pattern as the java.time classes use ISO 8601 formats by default.

OffsetDateTime

The input includes an offset-from-UTC with the +00:00 so we can parse as an OffsetDateTime object.

String input = "2015-02-05T05:20:02+00:00" ;
OffsetDateTime odt = OffsetDateTime.parse( input );

Math

If you want to add an hour or a minute later to set an alarm, call the plus methods.

OffsetDateTime minuteLater = odt.plusMinutes( 1 );
OffsetDateTime hourLater = odt.plusHours( 1 );

To get a count of milliseconds, go through the Instant class. The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds. Asking for milliseconds means possible data loss as the nine digits of a decimal fraction get truncated to the three digits of decimal fraction.

long millisecondsSinceEpoch = odt.toInstant().toEpochMilli();  // Warning: Possible loss of data in truncating nanoseconds to milliseconds.

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the old troublesome date-time classes such as java.util.Date, .Calendar, & java.text.SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to java.time.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations.

Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport and further adapted to Android in ThreeTenABP.

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • This is useful info, but I would qualify "Other answers are correct but now outdated. The old date-time classes are now legacy. Use java.time classes instead." There are plenty of cases where maintaining a dependency on a third-party backport is not worth the cost, vs. continuing to use the built-in classes where they're adequate. – LarsH Oct 20 '17 at 19:35
  • @LarsH Many of us, including Sun, Oracle, and the JCP community, would argue that the legacy classes are *not* adequate. Also, that third-party port comes with frequently updated [`tzdata` time zone database](https://en.wikipedia.org/wiki/Tz_database) which you should otherwise be [updating manually](http://www.oracle.com/technetwork/java/javase/tzupdater-readme-136440.html) in your no-longer updated JVM but are likely ignoring. – Basil Bourque Oct 20 '17 at 19:37
  • I agree, no doubt there are many cases where the legacy classes aren't adequate. But there are also many cases where they are (example: if your input datetime strings are fairly constrained and predictable and use UTC). And the cost of using something else is non-negligible. – LarsH Oct 20 '17 at 20:15