Java: 解析 ISO_DATE / ISO_OFFSET_DATE

Java: parse ISO_DATE / ISO_OFFSET_DATE

对于 REST Web 服务,我需要 return 带时区的日期(无时间)

显然 Java 中没有 ZonedDate 这样的东西(只有 LocalDateZonedDateTime),所以我使用 ZonedDateTime 作为后备。

将这些日期转换为 JSON 时,我使用 DateTimeFormatter.ISO_OFFSET_DATE 来格式化日期,效果非常好:

DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE;
ZonedDateTime dateTime = ZonedDateTime.now();
String formatted = dateTime.format(formatter);

2018-04-19+02:00

但是,尝试使用...

解析回这样的日期
ZonedDateTime parsed = ZonedDateTime.parse(formatted, formatter);

...导致异常:

java.time.format.DateTimeParseException: Text '2018-04-19+02:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {OffsetSeconds=7200},ISO resolved to 2018-04-19 of type java.time.format.Parsed

我也试过 ISO_DATE 和 运行 遇到同样的问题。

如何解析这样的划回日期?
或者是否有任何其他类型(在 Java 时间 API 内)我应该用于分区日期?

我找到了解决方案(使用 TemporalQueries):
分别解析日期和区域,并使用该信息恢复区域日期:

LocalDate date = formatter.parse(formatted, TemporalQueries.localDate());
ZoneId zone = formatter.parse(formatted, TemporalQueries.zone());
ZonedDateTime restored = date.atStartOfDay(zone);

问题是 ZonedDateTime 需要构建所有日期和时间字段(年、月、日、小时、分钟、秒、纳秒),但格式化程序 ISO_OFFSET_DATE 生成没有时间部分的字符串。

解析回来时,没有与时间相关的字段(小时、分钟、秒),你得到一个 DateTimeParseException.

解析它的另一种方法是使用 DateTimeFormatterBuilder 并为时间字段定义默认值。当您在回答中使用 atStartOfDay 时,我假设您想要午夜,因此您可以执行以下操作:

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    // date and offset
    .append(DateTimeFormatter.ISO_OFFSET_DATE)
    // default values for hour and minute
    .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
    .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
    .toFormatter();
ZonedDateTime parsed = ZonedDateTime.parse("2018-04-19+02:00", fmt); // 2018-04-19T00:00+02:00

也可以正常工作,但唯一的问题是您要解析输入 两次 (每次调用 formatter.parse 都会解析输入再次)。更好的替代方法是使用没有时间查询的 parse 方法(只解析一次),然后使用解析的对象来获取您需要的信息。

DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE;
// parse input
TemporalAccessor parsed = formatter.parse("2018-04-19+02:00");

// get data from the parsed object
LocalDate date = LocalDate.from(parsed);
ZoneId zone = ZoneId.from(parsed);
ZonedDateTime restored = date.atStartOfDay(zone); // 2018-04-19T00:00+02:00

使用此解决方案,输入仅被解析一次。

tl;博士

使用时区 (continent/region) 而不是单纯的与 UTC 的偏移量(小时-分钟-秒)。对于任何特定区域,偏移量可能会随时间变化。

结合两者来确定时刻。

LocalDate.parse(
  "2018-04-19"
)
.atStartOfDay( 
    ZoneId.of( "Europe/Zurich" ) 
) // Returns a `ZonedDateTime` object.

2018-04-19T00:00+02:00[Europe/Zurich]

从您的 REST 服务,或者:

  • Return 日期和时区分开(带分隔符或 XML/JSON),或者,
  • Return 一天的开始,因为这可能是带时区的日期的预期结果。

分隔文本输入

中的解决方案有效地将字符串输入视为一对字符串输入。首先提取并解析仅限日期的部分。其次,提取和解析与 UTC 的偏移量部分。因此,输入被解析两次,每次都忽略字符串的另一半。

我建议您将这种做法明确化。将日期作为一段文本进行跟踪,将偏移量(或者更好的是,时区)作为另一段文本进行跟踪。正如其他答案中的代码所示,在您采取下一步确定实际时刻(例如一天的开始)之前,带区域的日期没有真正的意义。

String inputDate = "2018-04-19" ;
LocalDate ld = LocalDate.parse( inputDate ) ;

String inputOffset = "+02:00" ;
ZoneOffset offset = ZoneOffset.of( inputOffset) ;

OffsetTime ot = OffsetTime.of( LocalTime.MIN , offset ) ; 
OffsetDateTime odt = ld.atTime( ot ) ;  // Use `OffsetDateTime` & `ZoneOffset` when given a offset-from-UTC. Use `ZonedDateTime` and `ZoneId` when given a time zone rather than a mere offset.

odt.toString(): 2018-04-19T00:00+02:00

如您所见,代码很简单,您的意图很明显。

并且无需为任何 DateTimeFormatter 对象或格式化模式操心。这些输入符合 ISO 8601 标准格式。 java.time 类 在 parsing/generating 字符串时默认使用这些标准格式。

偏移与区域

至于应用日期和偏移量来获取时刻,您正在混淆 offset-from-UTC with a time zone。偏移量只是小时数、分钟数和秒数。不多也不少。相反,时区是特定地区的人们使用的偏移量的过去、现在和未来变化的历史。

换句话说,+02:00恰好在许多日期被许多时区使用。但是在特定区域中,例如 Europe/Zurich,可能会在其他日期使用其他偏移量。例如,采用愚蠢的夏令时 (DST) 意味着一个区域将花费一半的时间使用一个偏移量,另一半时间使用不同的偏移量。

指定 proper time zone name in the format of continent/region, such as America/Montreal, Africa/CasablancaPacific/Auckland。切勿使用 ESTIST 等 3-4 字母缩写,因为它们 不是 真正的时区,未标准化,甚至不是唯一的(!)。

ZoneId z = ZoneId.of( "Europe/Zurich" ) ;  
ZonedDateTime zdt = ld.atStartOfDay( z ) ;

zdt.toString(): 2018-04-19T00:00+02:00[Europe/Zurich]

所以我建议你跟踪两个输入字符串:

  • 仅限日期 (LocalDate):YYYY-MM-DD 例如 2018-04-19
  • 正确的时区名称(ZoneId):continent/region 例如Europe/Zurich

合并。

ZonedDateTime zdt = 
    LocalDate.parse( inputDate )
             .atStartOfDay( ZoneId.of( inputZone ) ) 
;

注意:ZonedDateTime::toString 方法生成的字符串格式明智地扩展了标准 ISO 8601 格式,方法是在方括号中附加时区名称。这纠正了原本设计良好的标准所造成的巨大疏忽。但是如果您知道您的客户可以使用它,您只能通过 REST 服务 return 这样的字符串。


关于java.time

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

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

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

您可以直接与数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* 类.

在哪里获取java.time类?

  • Java SE 8, Java SE 9, Java SE 10,及以后
    • 内置。
    • 标准 Java API 的一部分,带有捆绑实施。
    • Java 9 添加了一些小功能和修复。
  • Java SE 6 and Java SE 7
  • Android
    • Android java.time 类.
    • 捆绑实施的更高版本
    • 对于较早的 Android (<26),ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See .

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.