Java 时间 API 将 ISO 8601 转换为 ZonedDateTime

Java Time API converting ISO 8601 to ZonedDateTime

我很难转换 UTC 格式的 ISO 8601 formatted String to a java.time.LocalDateTime

更具体地说,我正在尝试编写一个 XMLAdapter,您可以为其输入各种 ISO 8601 数据格式作为字符串(即 2002-09-242011-03-22T13:302015-05-24T12:25:15Z, 2015-07-28T11:11:15.321+05:30) 并以 UTC 格式输出 LocalDateTime,反之亦然。

系统以 UTC 时间存储所有内部日期和时间信息。当用户请求日期或时间时,它会根据他们自己的 ZoneId.

向用户表示
public class LocalDateTimeXmlAdapter extends XmlAdapter<String, LocalDateTime> {

    private static final Pattern ZONE_PATTERN = Pattern.compile("T.*(\+|\-|Z)");

    @Override
    public LocalDateTime unmarshal(String isoDateTime) throws Exception {
        LocalDateTime utcDateTime;
        if (ZONE_PATTERN.matcher(isoDateTime).matches()) {
            OffsetDateTime offsetDateTime = OffsetDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME);
            ZoneOffset offset = offsetDateTime.getOffset();
            utcDateTime = offsetDateTime.toLocalDateTime().plusSeconds(offset.getTotalSeconds());
        } else {
            LocalDateTime localDateTime = LocalDateTime.parse(isoDateTime, DateTimeFormatter.ISO_DATE_TIME);
            ZoneId zoneId = ZoneId.systemDefault(); // TODO: Get ZoneId from userProfile
            ZoneOffset offset = ZonedDateTime.now(zoneId).getOffset();
            utcDateTime = localDateTime.minusSeconds(offset.getTotalSeconds());
        }
        return utcDateTime;
    }

    @Override
    public String marshal(LocalDateTime utcDateTime) throws Exception {
        ZoneId zoneId = ZoneId.systemDefault(); // TODO: Get ZoneId from userProfile
        ZoneOffset offset = ZonedDateTime.now(zoneId).getOffset();
        return utcDateTime.plusSeconds(offset.getTotalSeconds()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }

}

编辑:罗勒下面的答案应该被标记为正确。

顾名思义,LocalDateTime 包含日期和时间。例如,您问题中的日期字符串的第一个示例仅包含有关日期的信息,因此您不能将其直接解析为 LocalDateTime。你可以做的是首先将它解析为 LocalDate 并通过设置该对象的时间得到 LocalDateTime.

LocalDateTime localDateTime = LocalDate.parse("2002-09-24").atStartOfDay();

所有日期和时间对象都有一个类似于LocalDate 的解析方法,它可以采用特定的字符串格式。这些格式是 DateTimeFormatter

中指定的不同 ISO 标准格式

要将自定义日期时间字符串格式化为 Temporal 对象,请使用 DateTimeFormatter 并指定自定义模式。

tl;博士

Instant.parse( "2015-05-24T12:25:15Z" )
       .atZone( 
           ZoneId.of( "America/Montreal" ) 
       )
       .toString()

2015-05-24T08:25:15-04:00[America/Montreal]

ZonedDateTime 对比 LocalDateTime

您的问题自相矛盾,标题要求 ZonedDateTime 而 body 要求 LocalDateTime。那是两种截然不同的野兽。一个 (ZonedDateTime) 是时间轴上的特定时刻,另一个 (LocalDateTime) 只是关于 可能 个时刻的模糊概念,但 不是一个特定的时刻。

例如,从今年开始的圣诞节 LocalDateTime2017-12-25T00:00:00,但在您应用时区之前,这没有任何意义,因为圣诞老人会首先在 Kiribati 的岛屿送货他们的午夜(地球上的第一个午夜),然后在他们晚些时候的午夜到达新西兰,然后在他们晚些时候的午夜到达澳大利亚,依此类推向西移动到连续的午夜。

此外,您的问题似乎对输入和输出到底是什么感到困惑。

a java.time.LocalDateTime which is in UTC.

这是一种自相矛盾的说法。 LocalDateTime class lacks any concept of time zone or offset-from-UTC. So this class cannot be used to represent a moment such as a UTC value. For a moment in UTC, use either Instant or OffsetDateTime classes.

Instant

对于像 2015-05-24T12:25:15Z 这样的输入,它表示 UTC 时间轴上的一个时刻(ZZulu 的缩写,表示 UTC)。要表示它,请使用 Instant class。 Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds(最多九 (9) 位小数)。

Instant instant = Instant.parse( "2015-05-24T12:25:15Z" );

通常,对于大多数业务逻辑、数据存储、数据交换和数据库,您应该在 UTC 中工作。因此,Instant 就是您所需要的。

ZonedDateTime

如果您需要调整时区,例如向用户展示,请应用 ZoneId 以获得 ZonedDateTime

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

ZoneId z = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = instant.atZone( z );

LocalDate

如果您有 date-only 值,请解析为 LocalDate object。

LocalDate localDate = LocalDate.parse( "2002-09-24" );

要获取该日期当天的第一时刻,请指定时区。对于任何给定时刻,世界各地的日期因地区而异。

此外,不要假设一天从 00:00:00 开始。夏令时 (DST) 等异常情况可能会导致第一时刻类似于 01:00:00。让java.time确定第一时刻的time-of-day.

ZonedDateTime zdt = localDate.atStartOfDay( z );

关于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 classes.

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

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

从哪里获得java.time classes?

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.

DateTimeFormatter 上定义了各种格式化程序来完成这项工作。

例如:

TemporalAccessor accessor= DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("2020-07-14T21:56:00Z");
ZonedDateTime from = ZonedDateTime.from(accessor);

或者在 iso 8601 规范中使用不同的偏移格式:

TemporalAccessor accessor= DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("2020-07-14T21:56:00+00:00");
ZonedDateTime from = ZonedDateTime.from(accessor);

注意: 这仅适用于 'extended' iso 8601 格式。我没有找到对简化版的支持(没有 - 例如 20200714T215600Z),但我认为这没什么大不了的。我只是不支持我的 API 中的简化格式。如果有人确实找到了一种方法来支持 iso 8601 简化日期格式(而不仅仅是扩展),那么工作量不大并且很乐意留下一个很棒的解决方案:)