如何在没有加载区域的情况下将 LocalDateTime 转换为 UTC 并返回?

How to convert LocalDateTime to UTC and back, without loading zones?

提问人:android developer 提问时间:4/11/2018 最后编辑:android developer 更新时间:4/17/2018 访问量:11299

问:

背景

我正在使用 android 的 threetenbp 向后移植这里),以处理各种与时间相关的数据操作。

其中之一是将时间转换为不同的时区(当前时区到 UTC 并返回)。

我知道如果你使用这样的东西,这是可能的:

LocalDateTime now = LocalDateTime.now();
LocalDateTime nowInUtc = now.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime();

这很好用,而且反其道而行之也很容易。

问题

我试图避免初始化库,这会将相当大的区域文件加载到其中。我已经弄清楚了如何在没有这个的情况下处理各种与日期/时间相关的操作,除了转换为 UTC 并返回的情况。

我得到的与正确转换相差整整 1 小时的错误。

我试过什么

这是我发现并尝试过的:

// getting the current time, using current time zone: 
Calendar cal = Calendar.getInstance();
LocalDateTime now = LocalDateTime.of(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY),
            cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), cal.get(Calendar.MILLISECOND) * 1000000);

//the conversion itself, which is wrong by 1 hour in my tests: 
LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(TimeZone.getDefault().getRawOffset() / 1000)).withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0))).toLocalDateTime();

问题

我写的东西到底有什么问题?如何在不初始化库的情况下获得用于转换时间的替代代码?

给定一个 LocalDateTime 实例作为输入,如何将其从当前时区转换为 UTC,以及从 UTC 转换为当前时区?

安卓 时区 UTC ThreeTenBP

评论

0赞 Meno Hochschild 4/11/2018
这两种方法并不等效,因为您要么使用 Threeten-BP 中嵌入的 tzdb-rules,要么使用平台区域规则,这些规则可能由于底层 tzdb 版本不同而有所不同。
0赞 android developer 4/11/2018
@MenoHochschild 由于该库应该移植 Java API,因此它应该与框架上的相同
0赞 Meno Hochschild 4/11/2018
哦不,如果你使用,那么你使用的是Android平台的tz-data(),而不是ThreetenABP()的tz-data。Calendar.getInstance()java.util.TimeZoneZoneId
0赞 Meno Hochschild 4/11/2018
并且使用不会完全抵消。你为什么不打电话?TimeZone.getDefault().getRawOffset()getOffset(...)
0赞 android developer 4/12/2018
@MenoHochschild 如果您有比公认的答案更好的解决方案,请发布。很难理解你在这里的意思。

答:

5赞 carlBjqsd 4/11/2018 #1

这可能是因为 JVM 的默认时区是夏令时 (DST)。

要获得正确的偏移量,您应该检查时区是否在 DST 中,并将其添加到偏移量中:

Calendar cal = Calendar.getInstance();

TimeZone zone = TimeZone.getDefault();
// if in DST, add the offset, otherwise add zero
int dst = zone.inDaylightTime(cal.getTime()) ? zone.getDSTSavings() : 0;
int offset = (zone.getRawOffset() + dst) / 1000;
LocalDateTime alternativeNowInUtc = now.atZone(ZoneOffset.ofTotalSeconds(offset))
    .withZoneSameInstant(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(0)))
    .toLocalDateTime();

创建 as a 的另一种方法是从 :nowInUtcLocalDateTimeInstantCalendar

LocalDateTime nowInUtc = Instant.ofEpochMilli(cal.getTimeInMillis())
    .atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();

实际上,您根本不需要,只需使用即可获取当前时刻:CalendarInstant.now()

LocalDateTime nowInUtc = Instant.now().atOffset(ZoneOffset.ofHours(0)).toLocalDateTime();

或者,更短一点,直接使用:OffsetDateTime

LocalDateTime nowInUtc = OffsetDateTime.now(ZoneOffset.ofHours(0)).toLocalDateTime();

不确定其中是否有任何加载时区数据,由您来测试。

而且我认为可以使用常量代替 ,因为它也不会加载 tz 数据(但我还没有测试过)。ZoneOffset.UTCZoneOffset.ofHours(0)

最终解决方案

假设默认时区在以色列 ( 是 ):TimeZone.getDefault()Asia/Jerusalem

// April 11th 2018, 3 PM (current date/time in Israel)
LocalDateTime now = LocalDateTime.of(2018, 4, 11, 15, 0, 0);

TimeZone zone = TimeZone.getDefault();
// translate DayOfWeek values to Calendar's
int dayOfWeek;
switch (now.getDayOfWeek().getValue()) {
    case 7:
        dayOfWeek = 1;
        break;
    default:
        dayOfWeek = now.getDayOfWeek().getValue() + 1;
}
// get the offset used in the timezone, at the specified date
int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1,
                            now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);
ZoneOffset tzOffset = ZoneOffset.ofTotalSeconds(offset / 1000);

// convert to UTC
LocalDateTime nowInUtc = now
                // conver to timezone's offset
                .atOffset(tzOffset)
                // convert to UTC
                .withOffsetSameInstant(ZoneOffset.UTC)
                // get LocalDateTime
                .toLocalDateTime();

// convert back to timezone
LocalDateTime localTime = nowInUtc
    // first convert to UTC
    .atOffset(ZoneOffset.UTC)
    // then convert to your timezone's offset
    .withOffsetSameInstant(tzOffset)
    // then convert to LocalDateTime
    .toLocalDateTime();
0赞 Meno Hochschild 4/14/2018 #2

carlBjqsd 的答案是好的,只是很尴尬,应该更清楚一点。

为什么相差一小时

查看 @carlBjqsd 的最终解决方案:它使用表达式

int offset = zone.getOffset(1, now.getYear(), now.getMonthValue() - 1, now.getDayOfMonth(), dayOfWeek, now.getNano() / 1000000);

而不是

getRawOffset().

这导致了你观察到的一小时的差异。应用程序通常不需要只使用原始偏移量进行计算,而原始偏移量会在一年中的某些时期省略 dst 偏移量。在从本地时间戳到 UTC 并返回的任何转换中,只有总偏移量才重要。对部分偏移(如原始偏移或 dst 偏移)进行细粒度区分的主要目的是正确命名区域(我们是否可以将其称为标准时间?

误导性问题标题:“无装载区”

可以,如果您想使用区域在本地时间戳和 UTC 之间进行转换,则永远无法避免加载区域。您真正的问题是:如何避免加载 ThreetenABP 的区域,而是使用/加载 Android 平台的区域。你的动机似乎是:

我试图避免初始化库,它加载了相当多的 将区域的大文件放入其中

好吧,我还没有衡量哪个区域数据对性能的影响更大。我只能说,根据我对相关库源代码的研究和知识,ThreetenBP 将整个文件加载到内存中的二进制数组缓存中(作为第一步),然后为单个区域挑选相关部分(即通过反序列化将二进制数据数组的一部分解释为一组区域规则,最后是单个)。旧的 Java 平台可以使用一组不同的 zi 文件(每个区域一个),我怀疑 Android 区域的行为方式类似(但如果您更了解该细节,请纠正我)。java.timeTZDB.datZoneId

如果只使用一个区域,那么使用单独区域文件的传统方法可能会更好,但是一旦您想遍历所有可用区域,那么最好只有一个区域文件。

就我个人而言,我认为性能方面是可以忽略的。如果您使用 Android 区域,您也不可避免地会有一些加载时间。如果你真的想加快ThreetenABP的初始化时间,你应该考虑在后台线程中加载它。

Android 区域和 ThreetenABP 区域是等效的吗?

一般不会。两个时区存储库可能会为具体区域提供相同的偏移量。他们经常这样做,但有时会有你无法控制的差异。尽管两个时区存储库最终都使用了 iana.org/tz 的数据,但差异主要是由于 tzdb-data 的不同版本造成的。而且您无法控制Android平台上存在哪个版本的区域数据,因为这取决于手机用户更新Android操作系统的频率。ThreetenABP的数据也是如此。您可以提供最新版本的应用程序,包括最新版本的 ThreetenABP,但您无法控制移动设备用户是否真的更新了应用程序。

关心选择合适的 tz 存储库的其他原因?

除了性能和初始化时间之外,确实有一种特殊场景可能会引起选择的兴趣。如果 Android 操作系统在某种程度上很旧并且使用过时的区域规则版本,那么一些手机用户不会更新他们的操作系统,而是操纵设备时钟以补偿错误的时区数据。这样,他们仍然可以在手机上获得正确的本地时间(在所有应用程序中)。

在这种情况下,ThreetenABP没有提供一个好的解决方案,因为将正确的区域数据与错误的设备时钟相结合将导致错误的本地时间戳(惹恼用户)。这一直是一个问题,例如在土耳其,它不久前改变了 dst 规则。

仅使用Android的旧日历和时区API(在软件包中)可以考虑该问题,因此可以创建正确的本地时间戳。但是,如果应用程序将 UTC 时间(例如,自 1970-01-01T00Z 以来的毫秒数)传达给其他主机(例如服务器),则错误的设备时钟仍然是一个问题。java.util

我们可以说为什么要打扰,因为用户对设备配置做了“胡说八道”,但我们也生活在现实世界中,应该考虑如何让这样的用户满意。因此,在考虑解决方案时,我至少在我的日历库中引入了 Time4A 方法,例如 SystemClock.inPlatformView(),它使用(可能)最实际的区域数据,并基于用户至少会遵守正确的本地设备时间的假设来获取正确的 UTC 时钟(无论他/她为实现这一目标做了什么, 通过更新操作系统或时钟/区域配置)。我很高兴以这种方式完全避免使用旧的日历和区域 API。我的 API 甚至允许同时使用两个区域存储库:

  • Timezone.of("java.util.TimeZone~Asia/Jerusalem")使用 Android 数据
  • Timezone.of("Asia/Jerusalem")使用 Time4A 数据

也许你可以从这些想法中获益,当你找到/开发合适的帮助类来使用ThreetenABP。Time4A 是开源的。

评论

0赞 android developer 4/17/2018
Time4A究竟能提供什么帮助?它是否有助于避免 ThreetenABP 库的初始化,但允许我使用与时区相关的类(例如 ZonedDateTime)而不必担心?初始化的问题在于它需要一些时间,并且在后台进行初始化是一个问题,因为您不知道何时需要使用它。也可能在初始化期间,然后它对性能没有任何帮助,因为您仍然需要等待它。问题是,我可以安全地避免库加载区域,并使用操作系统的区域吗?
0赞 Meno Hochschild 4/17/2018
@androiddeveloper 从逻辑上讲,Time4A 是 ThreetenABP 的替代品,而不是额外的。关于区域加载,如果你使用ThreetenABP,那么你就无法避免加载ThreetenABP的区域数据。如果您改用 Time4A,那么如果您想使用 Time4A 的区域数据(包括额外的延迟加载)或 Android 的区域数据(通过访问 )。但是,我可以想象引入一个中心配置参数,让用户始终抑制Time4A的内置区域数据(在不久的将来,会更方便)。java.util.TimeZone
0赞 android developer 4/17/2018
Time4A 是否提供与 ThreetenABP 相同的课程?我主要使用 LocalDate、LocalDateTime 和可悲的 ZonedDateTime
0赞 android developer 4/18/2018
为什么不提供与 Java 相同的类名和 API,而只是为它们添加功能呢?
0赞 android developer 4/18/2018
我并不是说使用相同的包名称。我的意思是使用相同的类名。就像在ThreetenABP上一样。这也有助于应用程序达到足够高的 minSdk 以使用内置 API :只需替换整个项目的“导入”部分中的包名称即可。