为什么 OffsetDateTime 无法解析 Java 8 中的 '2016-08-24T18:38:05.507+0000'

Why can't OffsetDateTime parse '2016-08-24T18:38:05.507+0000' in Java 8

表达式

OffsetDateTime.parse("2016-08-24T18:38:05.507+0000")

导致以下错误:

java.time.format.DateTimeParseException: Text '2016-08-24T18:38:05.507+0000' could not be parsed at index 23

另一方面,

OffsetDateTime.parse("2016-08-24T18:38:05.507+00:00")

按预期工作。

DateTimeFormatter's doc page 以不带冒号的区域偏移为例。我究竟做错了什么?我宁愿不破坏我的日期字符串来安抚 Java。

默认格式应为DateTimeFormatter.ISO_OFFSET_DATE_TIME,并使用以下区域偏移值定义:

static final OffsetIdPrinterParser INSTANCE_ID_Z = new OffsetIdPrinterParser("+HH:MM:ss", "Z");

尽管 DateTimeFormatter 的模式语言没有提供无法适应您的无冒号形式的区域偏移代码,但这并不意味着处理区域偏移的预定义实例接受 no -冒号形式。 OffsetDateTime.parse() 的单参数版本指定它使用 DateTimeFormatter.ISO_OFFSET_DATE_TIME 作为其格式化程序,并且该格式化程序的文档指定它支持三种格式,如 the docs of ZoneOffset.getId() 中所述。 None 这些格式(取自 ISO-8601)与您的无冒号格式一致。

但不用担心:只需使用 OffsetDateTime.parse() 的两个参数即可,提供适当的格式化程序。这有点不方便,但非常可行。

您正在调用以下方法。

public static OffsetDateTime parse(CharSequence text) {
    return parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

它将 DateTimeFormatter.ISO_OFFSET_DATE_TIME 用作 DateTimeFormatter,如 javadoc 中所述,它执行以下操作:

The ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30+01:00'.

如果你想解析一个与 2016-08-24T18:38:05.507+0000 格式不同的日期,你应该使用 OffsetDateTime#parse(CharSequence, DateTimeFormatter)。以下代码应该可以解决您的问题:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
OffsetDateTime.parse("2016-08-24T18:38:05.507+0000", formatter);

Paypal 错误地将偏移量发送为 +0000,这实际上与他们的规范 https://developer.paypal.com/docs/api/transaction-search/v1/ that says it has to be in an Internet date/time format 相矛盾,其中偏移量写为

 time-numoffset  = ("+" / "-") time-hour ":" time-minute

: 为必填项。

要解决此问题,应创建一个自定义日期时间格式化程序,但要避免使用像 yyyy-MM-dd'T'HH:mm:ssZ 这样的简单模式,因为如果毫秒突然出现在输出中,它将失败。

public static final DateTimeFormatter PAYPAL_DATE_TIME_FORMAT = new DateTimeFormatterBuilder()
    .parseCaseInsensitive()
    .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
    .parseLenient()
    .appendPattern("Z")
    .parseStrict()
    .toFormatter();

这是一种方法,但它有一个缺陷,即当 Paypal 更正其输出时,它将无法正确解析 : 偏移量。

此外,Paypal 不支持 nanos,因此您还应该 .truncatedTo(SECONDS) 在将其发送到他们的 API 之前

更新

感谢 Ole V.V。推荐这个更简单的模式:

DateTimeFormatter dtf = new DateTimeFormatterBuilder()
                        .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                        .appendPattern("[XXX][XX][X]")
                        .toFormatter(Locale.ENGLISH);

如果单位(例如月、日、小时等)可以是一位数或两位数,则原始答案仍然有用。如果单位是一位数,则此替代模式将失败。

原回答

解决方案是使用带有可选模式的 DateTimeFormatterDateTimeFormatter 允许我们在方括号中指定可选模式。

演示:

import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern(
                "u-M-d'T'H:m:s[.[SSSSSSSSS][SSSSSSSS][SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]][XXX][XX][X]",
                Locale.ENGLISH);
        
        // Test
        Stream.of(
                "2021-07-22T20:10:15+0000",
                "2021-07-22T20:10:15+00:00",
                "2021-07-22T20:10:15+00",
                "2021-07-22T20:10:15.123456789+0000",
                "2021-07-22T20:10:15.12345678+0000",
                "2021-07-22T20:10:15.123+0000",
                "2021-07-22T20:10:15.1+0000"                
        ).forEach(s -> System.out.println(OffsetDateTime.parse(s, dtf)));
    }
}

输出:

2021-07-22T20:10:15Z
2021-07-22T20:10:15Z
2021-07-22T20:10:15Z
2021-07-22T20:10:15.123456789Z
2021-07-22T20:10:15.123456780Z
2021-07-22T20:10:15.123Z
2021-07-22T20:10:15.100Z

输出中的 Z 是零时区偏移量的 timezone designator。它代表祖鲁语并指定 Etc/UTC 时区(时区偏移量为 +00:00 小时)。

Trail: Date Time.

了解有关现代日期时间 API 的更多信息

查看 documentation page of DateTimeFormatter 以获得模式字母的完整列表。