代码之家  ›  专栏  ›  技术社区  ›  android developer

如何将LocalDateTime转换为UTC并返回,而无需加载区域?

  •  2
  • android developer  · 技术社区  · 7 年前

    出身背景

    我正在使用 threetenbp backport 对于Android( here ),以处理各种与时间相关的数据操作。

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

    2 回复  |  直到 6 年前
        1
  •  4
  •   carlBjqsd    7 年前

    这可能是因为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 作为 LocalDateTime 是创建 Instant Calendar :

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

    实际上,你不需要 日历 总之,只要使用 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() 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();
    
        2
  •  0
  •   Meno Hochschild    7 年前

    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.time 然后再加载整个文件 TZDB.dat 进入内存中的二进制数组缓存(作为第一步),然后选择单个区域的相关部分(即通过反序列化将二进制数据数组的一部分转换为一组区域规则,最后转换为单个 ZoneId )。旧的Java平台使用的是一组不同的zi文件(每个区域一个),我怀疑Android区域的行为方式类似(但如果您更了解这个细节,请纠正我)。

    如果只应使用一个区域,则使用单独区域文件的传统方法可能更好,但一旦您希望迭代所有可用区域,则最好只使用一个区域文件。

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

    Android zones和ThreetenABP zones是否等效?

    通常不会。两个时区存储库可能会为具体区域提供相同的偏移量。他们经常这样做,但有时会有不受你控制的差异。 虽然两个时区存储库都使用 iana.org/tz 最终,差异主要是由tzdb数据的可能不同版本引起的。 您无法控制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") //使用Android数据
    • Timezone.of("Asia/Jerusalem") //使用Time4A数据

    也许您可以从这些想法中获益,什么时候可以找到/开发适合您使用ThreetenABP的助手类。Time4A是开源的。