Android 将字符串解析为日期 - 未知模式字符 'X'

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

Service 从网络上获取日期字符串,然后我想将其削减为 Date 对象。但是不知何故应用程序崩溃了。 这是我正在解析的字符串: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.access00(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

编辑: onDestroy() 为定期更新设置警报...

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

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

一切都会正常进行。

浏览可在 SimpleDateFormat 构造函数中使用的符号列表。尽管 documentation 显示 "XXX" 格式,但这不适用于 Android 并且会抛出 IllegalArgumentException.

可能您正在寻找 "yyyy-MM-dd'T'HH:mm:ss.SSSZ"

将您的代码更改为

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

DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); // if timezone is required

错误是说 simpleDateFormat 无法识别字符 X。如果您要查找毫秒,则用字符 S 表示。

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

您使用了错误的日期格式化程序。

改用这个:DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

我认为 android 与 Java 7 相比,时区使用 Z(如 Java 6)而不是 X。因此,使用 this 作为您的时区日期格式。

由于 Z 和 XXX 不同,我实施了以下解决方法:

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

以下 class 可用于在模式不知道的情况下将字符串转换为日期。

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

Android SimpleDateFormat不同于Java7 SDK,不支持'X'解析ISO 8601,您可以使用'Z'或'ZZZZZ' 样式来格式化并以编程方式将时区设置为 UTC。这是一个实用程序 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);
    }
}

SimpleDateFormat 的 Android 版本不支持 X 模式,因此 XXX 将不起作用,但您可以使用 ZZZZZ执行相同操作并以 +02:00 格式(或 -02:00 取决于当地时区)输出时区。

tl;博士

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.

详情

其他答案是正确的,但现在已经过时了。旧的日期时间 classes 现在是遗留的。请改用 java.time classes。

ISO 8601

输入字符串采用标准 ISO 8601 格式。直接解析,不需要定义格式,java.time class默认使用ISO 8601格式

OffsetDateTime

输入包含与 +00:00 的 UTC 偏移量,因此我们可以将其解析为 OffsetDateTime 对象。

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

数学

如果要在一小时或一分钟后设置闹钟,请调用plus方法。

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

要计算毫秒数,请查看 Instant class。 Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds。要求毫秒意味着可能的数据丢失,因为小数的九位被截断为小数的三位。

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

关于java.time

java.time 框架内置于 Java 8 及更高版本中。这些 classes 取代了旧的麻烦的日期时间 classes,例如 java.util.Date.Calendarjava.text.SimpleDateFormat

Joda-Time project, now in maintenance mode,建议迁移到java.time。

要了解更多信息,请参阅 Oracle Tutorial。并在 Stack Overflow 中搜索许多示例和解释。

许多 java.time 功能被反向移植到 ThreeTen-Backport and further adapted to Android in ThreeTenABP 中的 Java 6 和 7。

ThreeTen-Extra 项目用额外的 class 扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。

简单的解决方案:

使用这个yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ

而不是yyyy-MM-dd'T'HH:mm:ss.SSSXXX

完成。

使用 SimpleDateFormat 生成格式正确的 String 输出:

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

输出:2018-02-27T07:36:47.686Z

没有人提到在 pre-nougat 设备 上发生的这个错误,所以我想分享我的答案,也许它对那些因此到达这个线程的人有帮助。

这个 answer rightly mentions that "X" is supported only for Nougat+ devices. I still see that documentation 建议使用 "yyyy-MM-dd'T'HH:mm:ss.SSSXXX" 并且不确定他们为什么不明确指出这一点。

对我来说,yyyy-MM-dd'T'HH:mm:ssXXX 工作正常,直到我尝试在 6.0 设备上测试它并且它开始崩溃,这让我开始研究这个主题。将其替换为 yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ 已解决问题并适用于所有 5.0+ 设备。

根据 android documentation X 格式的区域偏移在 API 级别 24+

中得到支持
Letter  Date or Time Component      Supported (API Levels)
X       Time zone                   24+

所以我们不能使用较低的 APIs,我找到了解决这个问题的方法:

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

基于 的想法,我对其进行了优化并支持从和到 UTC 时区格式的解析,如 1970-01-01T00:00:00Z,以使所有行为与 yyyy-MM-dd'T'HH:mm:ssXXX.[=15 完全相同=]

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