为什么 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);
如果单位(例如月、日、小时等)可以是一位数或两位数,则原始答案仍然有用。如果单位是一位数,则此替代模式将失败。
原回答
解决方案是使用带有可选模式的 DateTimeFormatter
。 DateTimeFormatter
允许我们在方括号中指定可选模式。
演示:
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
小时)。
了解有关现代日期时间 API 的更多信息
查看 documentation page of DateTimeFormatter
以获得模式字母的完整列表。
表达式
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);
如果单位(例如月、日、小时等)可以是一位数或两位数,则原始答案仍然有用。如果单位是一位数,则此替代模式将失败。
原回答
解决方案是使用带有可选模式的 DateTimeFormatter
。 DateTimeFormatter
允许我们在方括号中指定可选模式。
演示:
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
小时)。
查看 documentation page of DateTimeFormatter
以获得模式字母的完整列表。