如何使用Java解析RFC 3339格式的日期时间?

38

我正在尝试解析从HTML5的datetime输入字段返回的日期值。在Opera中尝试一下就可以看到一个例子。返回的日期看起来像这样:2011-05-03T11:58:01Z

我想将其解析为Java Date或Calendar对象。

理想情况下,解决方案应具备以下特点:

  • 没有外部库(JAR)
  • 处理所有可接受的RFC 3339格式
  • 可以轻松验证字符串是否是有效的RFC 3339日期

1
为什么要求没有外部库?Joda Time只是这样做。 - Ian McLaird
我确实喜欢Joda Time。但这是框架的一部分,我不想将Joda Time作为框架的依赖添加进来。 - Adam
1
FYI,Joda-Time 项目现在处于维护模式,团队建议迁移到 java.time 类。请参见Oracle的教程 - Basil Bourque
1
请注意,像java.util.Datejava.util.Calendarjava.text.SimpleDateFormat这样的老旧日期时间类现在已经成为遗留系统,被内置于Java 8及更高版本中的java.time类所取代。请参阅Oracle的教程 - Basil Bourque
9个回答

39

简而言之

Instant.parse( "2011-05-03T11:58:01Z" )

ISO 8601

RFC 3339 仅是对实际标准 ISO 8601 的自我宣称“配置文件”。

与 ISO 8601 不同的是,RFC 故意违反 ISO 8601 以允许零小时的负偏移(-00:00),并赋予其语义含义“偏移未知”。在我看来,这种语义似乎是一个非常糟糕的想法。建议坚持更明智的 ISO 8601 规则。在 ISO 8601 中,没有任何偏移意味着偏移未知 - 这是一个显而易见的含义,而 RFC 规则则是深奥的。

java.time 类现代化,使用解析/生成字符串时默认使用 ISO 8601 格式。

您的输入字符串表示 UTC 中的一个时刻。结尾的 Z 缩写为 Zulu,表示 UTC。

Instant(不是 Date

现代类 Instant 表示 UTC 中的一个时刻。该类替换了 java.util.Date,并使用纳秒而不是毫秒的更高分辨率。

Instant instant = Instant.parse( "2011-05-03T11:58:01Z" ) ;

ZonedDateTime(而不是 Calendar

为了使用某个地区(时区)人们使用的挂钟时间查看相同的时刻,请应用 ZoneId 以获取 ZonedDateTime。此类 ZonedDateTime 取代了 java.util.Calendar 类。

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;  // Same moment, same point on the timeline, different wall-clock time.

转换

我强烈建议尽可能避免使用旧的日期时间类。但如果你必须与尚未更新到 java.time 的旧代码进行交互,你可以进行来回转换。调用添加到类中的新方法。

Instant 替代了 java.util.Date

java.util.Date myJUDate = java.util.Date.from( instant ) ;  // From modern to legacy.
Instant instant = myJUDate.toInstant() ;                    // From legacy to modern.

ZonedDateTime 取代了 GregorianCalendar

java.util.GregorianCalendar myGregCal = java.util.GregorianCalendar.from( zdt ) ;  // From modern to legacy.
ZonedDateTime zdt = myGregCal.toZonedDateTime() ;           // From legacy to modern.

如果你有一个实际上是GregorianCalendarjava.util.Calendar,进行强制类型转换。
java.util.GregorianCalendar myGregCal = ( java.util.GregorianCalendar ) myCal ;  // Cast to the concrete class.
ZonedDateTime zdt = myGregCal.toZonedDateTime() ;           // From legacy to modern.

要点

关于你的问题的特定问题...

  • 没有外部库(jars)

java.time类内置于Java 8、9、10及更高版本。实现也包含在后来的Android中。对于早期的Java和早期的Android,请参见本答案的下一节。

  • 处理所有可接受的RFC 3339格式

各种java.time类处理我所知道的每个ISO 8601格式。它们甚至处理了一些在标准的后续版本中神秘消失的格式。

对于其他格式,请参见各种类的parsetoString方法,例如LocalDateOffsetDateTime等。此外,在Stack Overflow上搜索,因为有许多关于这个主题的示例和讨论。

  • 一个字符串应该能够轻松地验证它是否是有效的RFC 3339日期

要验证输入字符串,请捕获DateTimeParseException

try {
    Instant instant = Instant.parse( "2011-05-03T11:58:01Z" ) ;
} catch ( DateTimeParseException e ) {
    … handle invalid input
}

关于 java.time

java.time框架内置于Java 8及更高版本中。这些类替代了老旧的legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现在处于maintenance mode维护模式,并建议迁移到java.time类。

要了解更多信息,请参见Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310

您可以直接使用与JDBC 4.2或更高版本兼容的JDBC驱动程序,无需字符串或java.sql.*类,即可直接在数据库中交换java.time对象。

如何获取java.time类?

ThreeTen-Extra项目通过添加额外的类来扩展java.time。该项目是java.time可能未来增加内容的试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuartermore


5
很不幸,“Instant”在带有时区的日期上一直失败,例如“2019-11-30T10:30:00+02:00”或“2019-10-12T07:20:50.52+00:00”,包括毫秒和不包括毫秒的日期。它会出现诸如“java.time.format.DateTimeParseException:Text'2019-11-30T10:30:00+02:00'could not be parsed at index 19”或同样位置的错误。 因此我无法在生产中使用它,它实际上不支持RFC-3339。 - Dmitriy Popov
2
@DmitriyPopov OffsetDateTime.parse( "2019-11-30T10:30:00+02:00" )OffsetDateTime.parse( "2019-10-12T07:20:50.52+00:00" )。所有的 java.time 类默认使用 ISO 8601 格式来解析/生成文本。您只需要选择适合语义的 java.time 类即可。 - Basil Bourque
1
说需要选择适当的java.time类来处理文本的语义有点不真诚。这些语义都是有效的RFC3339,你似乎在说我需要先解析它才能确定要使用哪个类来解析它? - Daniel C. Sobral
@DanielC.Sobral 使用适合每种值的类。对于UTC值,请使用“Instant”。对于带有偏移量的值,请使用“OffsetDateTime”。对于带有时区的值,请使用“ZonedDateTime”。对于缺少任何偏移或时区的值,请使用“LocalDateTime”。这真的不是很复杂。那怎么会是“不诚实”的呢? - Basil Bourque
这是不真诚的,因为确定一个值是UTC还是偏移量是解析的一部分。如果我有一个带有“RFC3339时间戳”字段的协议,那么在解码它时必须接受所有有效的RFC3339值。可惜,命名时区不是有效的RFC3339。 - Daniel C. Sobral
这是不诚实的,因为确定一个值是UTC还是偏移量是解析的一部分。如果我有一个协议,其中一个字段说“RFC3339时间戳”,那么在解码时必须接受所有有效的RFC3339值。然而,命名的时区并不是有效的RFC3339值。 - Daniel C. Sobral

16
在原则上,这可以使用不同的SimpleDateFormat模式完成。
以下是RFC 3339中individual declarations的模式列表:
  • date-fullyear: yyyy(年份)
  • date-month: MM(月份)
  • date-mday: dd(日期)
  • time-hour: HH(小时)
  • time -minute: mm(分钟)
  • time-second: ss(秒数)
  • time-secfrac: .SSS(毫秒,不过不清楚如果有超过或少于3位数字会发生什么)
  • time-numoffset: (像+02:00这样的格式似乎不被支持,而是支持+0200GMT+02:00和一些使用zZ的命名时区。)
  • time-offset: 'Z'(不支持其他时区)- 在使用此项之前应该使用format.setTimezone(TimeZone.getTimeZone("UTC"))
  • partial-time: HH:mm:ssHH:mm:ss.SSS
  • full-time: HH:mm:ss'Z'HH:mm:ss.SSS'Z'
  • full-date: yyyy-MM-dd(完整日期)
  • date-time: yyyy-MM-dd'T'HH:mm:ss'Z'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'(完整日期和时间)

我们可以看到,这似乎无法解析所有内容。也许从头开始实现一个RFC3339DateFormat会是一个更好的想法(使用正则表达式,以简便为主,或手动解析,以提高效率)。


“date-fullyear” 应该不是 “yyyy” 吗? - Buhake Sindi
看起来你就在这里没错了 - EEEE 代表"星期几"。我不确定我从哪里得到的,也许是来自另一个库为 y 做了不同的事情? - Paŭlo Ebermann
使用Java 7+,SimpleDateFormat添加了X时区格式,它是Z格式的超集,接受+/-两位或四位数字,带有可选的冒号(但忽略了分钟)。 - karmakaze
3
SimpleDateFormat 无法处理毫秒之外的小数位。 - mmindenhall
1
@Thomas 虽然我正在使用Java 8,但我从未需要使用它解析日期(因为我一直在使用HTTP和/或Json映射的库进行解析)。您可以添加自己的答案,我会给它点赞。(看起来DateTimeFormatter.ISO_OFFSET_DATE_TIME包括这个功能。) - Paŭlo Ebermann
@Thomas 是的,我发布了一个答案,展示了使用内置于Java 8、9、10及更高版本的java.time类的现代方法。传统的日期时间类确实是一个糟糕的设计混乱不堪(无论意图多好),应该避免使用。 - Basil Bourque

13

刚刚发现谷歌在Google HTTP客户端库中实现了Rfc3339解析器。

https://github.com/google/google-http-java-client/blob/dev/google-http-client/src/main/java/com/google/api/client/util/DateTime.java

经过测试,它可以很好地解析各种子秒时间片段。

import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;

import com.google.api.client.util.DateTime;

DateTimeFormatter formatter = DateTimeFormatter
            .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
            .withZone(ZoneId.of("UTC"));

@Test
public void test1e9Parse() {
    String timeStr = "2018-04-03T11:32:26.553955473Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.553Z");
}

@Test
public void test1e3Parse() {
    String timeStr = "2018-04-03T11:32:26.553Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.553Z");
}

@Test
public void testEpochSecondsParse() {

    String timeStr = "2018-04-03T11:32:26Z";

    DateTime dateTime = DateTime.parseRfc3339(timeStr);
    long millis = dateTime.getValue();

    String result = formatter.format(new Date(millis).toInstant());

    assert result.equals("2018-04-03T11:32:26.000Z");
}

3
您似乎正在使用 java.time 类(很好)。但是在 java.time 中没有 DateTime 类。我感到困惑。 - Basil Bourque
1
抱歉,我忘记写DateTime类的导入。它属于com.google.api.client.util包。 - vicknite

2

如果您拥有的格式为2011-05-03T11:58:01Z,那么下面的代码将起作用。但是,最近我在Chrome和Opera中尝试了html5 datetime,它给出的结果是2011-05-03T11:58Z——没有ss部分,这无法被下面的代码处理。

new Timestamp(javax.xml.datatype.DatatypeFactory.newInstance().newXMLGregorianCalendar(date).toGregorianCalendar().getTimeInMillis());

2

也许不是最优雅的方法,但肯定是我最近制作的一个可行方法:

Calendar cal = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
cal.setTime(sdf.parse(dateInString.replace("Z", "").replace("T", "-")));

1

虽然这个问题很老,但是它可能会帮助想要 Kotlin 版本答案的人。使用这个文件,任何人都可以将 Rfc3339 日期转换为任何日期格式。在这里,我使用一个空文件名 DateUtil 并创建一个名为 getDateString() 的函数,该函数有 3 个参数。

1st argument : Your input date
2nd argument : Your input date pattern
3rd argument : Your wanted date pattern

DateUtil.kt

object DatePattern {
    const val DAY_MONTH_YEAR = "dd-MM-yyyy"
    const val RFC3339 = "yyyy-MM-dd'T'HH:mm:ss'Z'"
}

fun getDateString(date: String, inputDatePattern: String, outputDatePattern: String): String {
    return try {
        val inputFormat = SimpleDateFormat(inputDatePattern, getDefault())
        val outputFormat = SimpleDateFormat(outputDatePattern, getDefault())

        outputFormat.format(inputFormat.parse(date))
    } catch (e: Exception) {
        ""
    }
}

现在,在您的活动/函数/数据源映射器中使用此方法以获取字符串格式的日期,如下所示:

getDate("2022-01-18T14:41:52Z", RFC3339, DAY_MONTH_YEAR)

输出将会像这样

18-01-2022

1

作为备选方案,未来您可以使用ITU[1],它是手写的,用于处理RFC-3339解析并让您轻松处理闰秒。该库无需依赖其他库,仅占用18 kB。

完全公开透明:我是作者

try 
{
    final OffsetDateTime dateTime = ITU.parseDateTime(dateTimeStr);
}
catch (LeapSecondException exc) 
{
  // The following helper methods are available let you decide how to progress
  //int exc.getSecondsInMinute()
  //OffsetDateTime exc.getNearestDateTime()
  //boolean exc.isVerifiedValidLeapYearMonth()
}

[1] - https://github.com/ethlo/itu


1
我正在使用这个:
DateTimeFormatter RFC_3339_DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
            .append(ISO_LOCAL_DATE_TIME)
            .optionalStart()
            .appendOffset("+HH:MM", "Z")
            .optionalEnd()
            .toFormatter();

例子:

String dateTimeString = "2007-05-01T15:43:26.3452+07:00";
ZonedDateTime zonedDateTime = ZonedDateTime.from(RFC_3339_DATE_TIME_FORMATTER.parse(dateTimeString));

谢谢分享!我有一个工具,仍然必须支持Java 8,并且我刚刚发现偏移模式“+HH:MM”在JDK8中不是标准的。然而,在早期的JDK版本中(我在Java 17上尝试过),只需调用Instant.parse(string)即可支持两个偏移选项。 - Oswaldo Junior

-1
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(datetimeInFRC3339format)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接