2

背景

我正在使用适用于 Android 的threetenbp backport此处)来处理各种与时间相关的数据操作。

其中之一是将时间转换为不同的时区(当前为 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 转换为当前时区?

4

2 回答 2

4

这可能是因为您的 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();

nowInUtc创建as a的另一种方法LocalDateTimeInstantCalendar:

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

实际上,您根本不需要Calendar,只需用于Instant.now()获取当前时刻:

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

或者,甚至更短,OffsetDateTime直接使用:

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

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

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

最终解决方案

假设默认时区在以色列(TimeZone.getDefault()is 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();
于 2018-04-11T11:21:48.880 回答
0

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 平台的区域。你的动机似乎是:

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

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

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

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

Android 区域和 ThreetenABP 区域是否等效?

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

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

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

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

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

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

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

当您找到/开发适合您使用 ThreetenABP 的辅助类时,也许您可​​以从这些想法中受益。Time4A 是开源的。

于 2018-04-14T03:57:49.703 回答