Android parse String to Date - unknown pattern character 'X'

JavaAndroidDateSimpledateformat

Java Problem Overview


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)
);

Java Solutions


Solution 1 - Java

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).

Solution 2 - Java

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

Solution 3 - Java

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.

Solution 4 - Java

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.

Solution 5 - Java

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);
    }
}

Solution 6 - Java

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);
    }
}

Solution 7 - Java

Simple solution:

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

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

Done.

Solution 8 - Java

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()
}

Solution 9 - Java

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");


Solution 10 - Java

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

Solution 11 - Java

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());
}

Solution 12 - Java

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;
	}
}

Solution 13 - Java

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatjažView Question on Stackoverflow
Solution 1 - JavaMickäel A.View Answer on Stackoverflow
Solution 2 - JavaRohit5k2View Answer on Stackoverflow
Solution 3 - JavaWahib Ul HaqView Answer on Stackoverflow
Solution 4 - JavaThanosView Answer on Stackoverflow
Solution 5 - JavaAlexander KView Answer on Stackoverflow
Solution 6 - JavaNarekView Answer on Stackoverflow
Solution 7 - JavaHiren PatelView Answer on Stackoverflow
Solution 8 - JavabeigiradView Answer on Stackoverflow
Solution 9 - JavaAdam WView Answer on Stackoverflow
Solution 10 - JavasuhasiniView Answer on Stackoverflow
Solution 11 - JavakxfengView Answer on Stackoverflow
Solution 12 - JavaSudhakarView Answer on Stackoverflow
Solution 13 - JavaBasil BourqueView Answer on Stackoverflow