【问题标题】:How to Pick Timezone from ISO 8601 format String into a Calendar instace如何从 ISO 8601 格式字符串中选择时区到日历实例中
【发布时间】:2018-02-13 21:09:03
【问题描述】:

作为输入,我有一个字符串,它是 ISO 8601 中的字符串,用于表示日期。例如:

“2017-04-04T09:00:00-08:00”

String 的最后一部分 "-08:00" 表示时区偏移。我将此字符串转换为Calendar 实例,如下所示:

Calendar calendar = GregorianCalendar.getInstance();
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).parse(iso8601Date);
calendar.setTime(date);

iso8601Date 是“2017-04-04T09:00:00-08:00”

但这不会选择时区,如果我从Calendar 实例获取时区,它会提供当前设置的笔记本电脑实例,并且不会从 ISO 8601 字符串中获取时间戳。我通过日历实例检查时区:​​

calendar.getTimeZone().getDisplayName()

有人可以在Calendar 实例中展示如何选择时区吗?

【问题讨论】:

    标签: java android date datetime-parsing java.util.calendar


    【解决方案1】:

    tl;博士

    OffsetDateTime.parse( "2017-04-04T09:00:00-08:00" ) 
    

    详情

    字符串的最后一部分“-08:00”表示时区偏移。

    不要将偏移量与时区混淆。

    -08:00 代表 offset-from-UTC,而不是 time zone。时区是特定地区的人们在过去、现在和未来使用的各种偏移的历史记录。时区以大陆、斜线和区域命名,例如 America/Los_AngelesPacific/AucklandAsia/Kolkata

    您正在使用麻烦的旧日期时间类,现在已被 java.time 类取代。对于 Android,请参阅 ThreeTen-BackportThreeTenABP 项目。

    您的输入仅指示偏移量而不是区域。所以我们解析为OffsetDateTime

    OffsetDateTime odt = OffsetDateTime.parse( "2017-04-04T09:00:00-08:00" ) ;
    

    如果您完全确定预期的时区,请指定它。

    ZoneId z = ZoneId.of( "America/Los_Angeles" ) ;
    ZonedDateTime zdt = odt.atZoneSameInstant() ;
    

    关于java.time

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

    Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

    要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

    从哪里获得 java.time 类?

    【讨论】:

    • 我同意,远离java.util.Date(除非某些其他 API 绝对要求并且您在本地时区工作)。
    • @EugenPechanec 实际上,java.util.Date 始终采用 UTC,因此您自己的时区将无关紧要。 DateCalendar 的课程都是一团糟,我建议始终避开它们;始终使用 java.time 或其后端端口。当其他 API 需要遗留类时,请在 java.time 中自行处理,然后转换结果。要在 Java 8 及更高版本中进行转换,请查看添加到旧类的新方法,在向后移植中查看具有转换方法的实用程序类。
    • 我读到了 java.time.OffsetDateTime 但它不适用于 android。
    • @Terri 查看我的答案中链接的后端项目。非常值得为您的项目添加一个库。
    • @BasilBourque 作为返回单个字段(getHour()getDay() 等)的一部分,日历将“标准化”为当前时区。因此,如果您使用 millis 构造函数(即在 UTC 中)构造 Date 对象,您会得到垃圾,除非您的本地 TZ 是 UTC。如果您使用单个字段构造 Date 对象,那么如果您的本地 TZ 不同,您将得到垃圾。总之,它不是UTC,甚至不是本地TZ。这是一个不确定的血腥混乱。
    【解决方案2】:

    当您创建 Calendar 时,它会采用 JVM 的默认时区。当您将String 解析为Date 时,它只设置一个值:自纪元以来的毫秒数(1970-01-01T00:00Z)。一个Datedoesn't have any timezone information,就是这个毫秒值。所以你需要在日历中设置时区。

    在您的格式化程序中,您将 Z 视为文字,因为它在引号内 ('Z')。这会忽略偏移量并获取 JVM 默认时区中的日期(如果相应的偏移量不是 -08:00,则会有不同的值)。

    在 JDK >= 7 中,可以使用X 模式来解析偏移量:

    Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.US).parse(iso8601Date);
    

    但这不会在日历中设置时区(它仍将使用 JVM 的默认值)。因此,“更好”的方法是从输入中去除偏移量并单独处理:

    Calendar calendar = GregorianCalendar.getInstance();
    String iso8601Date = "2017-04-04T09:00:00-08:00";
    // get the offset (-08:00)
    String offset = iso8601Date.substring(19);
    TimeZone tz = TimeZone.getTimeZone("GMT" + offset);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
    // set the offset in the formatter
    sdf.setTimeZone(tz);
    // parse just date and time (without the offset)
    Date date = sdf.parse(iso8601Date.substring(0, 19));
    // set the offset in the calendar
    calendar.setTimeZone(tz);
    calendar.setTime(date);
    

    这样,日历将设置偏移量-08:00。正如@BasilBourque's answer 已经说过的那样,-08:00 is an offset, not a timezoneTimeZone 类将偏移量视为时区,这是一种解决方法/错误的设计选择)。


    Java 新日期/时间 API

    旧的类(DateCalendarSimpleDateFormat)有 lots of problemsdesign issues,它们正在被新的 API 取代。

    在 Android 中,您可以使用 ThreeTen Backport,这是 Java 8 新日期/时间类的一个很好的反向移植。您还需要ThreeTenABP 才能使其工作(更多关于如何使用它here)。

    @BasilBourque's answer 已经告诉你OffsetDateTime。但要转换为Calendar,您可以使用org.threeten.bp.ZonedDateTime 并使用org.threeten.bp.DateTimeUtils 类进行转换:

    String iso8601Date = "2017-04-04T09:00:00-08:00";
    ZonedDateTime zdt = ZonedDateTime.parse(iso8601Date);
    Calendar cal = DateTimeUtils.toGregorianCalendar(zdt);
    

    日历已经设置了-08:00 偏移量。


    如果要从偏移量中获取时区,恐怕没那么简单。多个时区can use the same offset,因此您无法确定要使用哪个时区(您能做的最好的事情就是获得一份可能的候选人列表)。


    java.util.Date

    只是关于java.util.Date 的更详细说明。 This link 解释了很多,所以我真的推荐你阅读它。

    如上所述,Date 没有时区信息。它只保留自纪元以来的毫秒数(即 1970-01-01T00:00Z,或 1970 年 1 月 1 日st UTC 午夜 1970 年)。

    这个值在世界各地都是一样的。示例:在我写这篇文章的那一刻,当前时间的毫秒值是1504632865935。这个数字对于世界上与我在同一时刻获得当前时间的任何人都是相同的,无论他们使用的是哪个时区。

    不同的是与此毫秒值对应的本地日期和时间。在UTC对应2017-09-05T17:34:25.935Z,在纽约,日期相同(September 5th 2017)但时间不同(13:34),在东京是9月6th 2017 年凌晨 02:34。

    虽然Date 对象是相同的(因为它对每个人的millis 值都是1504632865935),但对应的日期和时间会根据所使用的时区而变化。

    人们倾向于认为Date 有一个时区,因为在打印它(使用System.out.println 或通过日志记录)或在调试器中检查时,它隐式使用toString() 方法,这会将日期转换为JVM 的默认时区(它还打印时区名称)。这给人的印象是Date 设置了格式和时区,但实际上没有。

    【讨论】:

    • 我希望我能多加一个。
    【解决方案3】:

    我想从 Hugo 的回答中分享一个关键的理解,我的进一步搜索如下。如果我错了,请纠正我:

    日期不关心时区。它表示自纪元以来经过的毫秒数。

    关于从提供的 ISO 8061 格式中找到时区,Date 类无法分辨,我们必须使用@Hugo 和@Basil Bourque 指定的一些替代方法。

    【讨论】:

    • 不完全是。 Date 具有自 1970-01-01T00:00Z 以来的毫秒数(也称为“unix 纪元”,或 UTC 时间 1970 年 1 月 1 日午夜)。假设这个毫秒数是 1491325200000。这个数字代表每个时区的不同日期和时间。可以是2017-04-04 at 5 PM in UTC,也可以是纽约下午 1 点或东京第二天凌晨 2 点 (2017-04-05)。 Date 不知道日期/时间字段(它没有日/月/年,也没有小时/分钟/秒 - 这些字段在不同的时区会有不同的值 - 因为日期没有时区,所以没有字段)。
    • 当你说“毫秒代表例如:2017年1月1日02:00”时,你还必须问:“在哪里?” (在哪个时区?)。此日期和时间可以表示自纪元以来的不同毫秒数,具体取决于您使用的时区。如果您考虑 2017 年 1 月 1 日凌晨 2 点的 UTC,那么毫秒值是 1483236000000。但是纽约的相同日期/时间对应于millis 1483254000000,而在东京则是1483203600000。 millis 值是唯一的“绝对”值(在任何地方都相同),但与之对应的本地日期/时间取决于时区。
    • 我在回答中添加了一些关于它的信息。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 1970-01-01
    • 2015-08-10
    • 2014-10-12
    • 2011-02-04
    相关资源
    最近更新 更多