从 SimpleTimeZone 获取 ZoneId

Getting ZoneId from a SimpleTimeZone

使用 Java 我有一个 SimpleTimeZone 实例,其中包含来自遗留系统的 GMT 偏移和夏令时信息。

我想检索 ZoneId 以便能够使用 Java 8 次 API。

实际上,toZoneId returns ZoneId 没有夏令时信息

SimpleTimeZone stz = new SimpleTimeZone( 2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY,1,1,1, Calendar.FEBRUARY,1,1,1, 1 * 60 * 60 * 1000);
stz.toZoneId();

我认为这是不可能的。人们可能还会问这有什么意义。如果一个时区不在 Olson 数据库中,那么它在现实世界中几乎不会被使用,那么它在您的程序中有什么用处呢?当然,我理解您的遗留程序创建 SimpleTimeZone 的情况 确实 代表在野外使用的时区,但只是给它一个不正确的 ID,以便 TimeZone.toZoneId() 无法做出正确的翻译。

我查看了TimeZone.toZoneId()的源代码。它完全依赖于从 getID 方法获得的值。它不考虑偏移量或区域规则。重要的是,SimpleTimeZone 不会覆盖该方法。因此,如果您的 SimpleTimeZone 具有已知 ID(包括令人不悦的缩写,如 EST 或 ACT),那么您将获得正确的 ZoneId。否则你不会。

所以我想你最好的办法是找出你的遗留代码试图给你的时区的正确时区 ID,然后从 [=18= 得到你的 ZoneId ].或者稍微好一点,构建您自己的别名映射,其中包含遗留代码中的一个或多个 ID 以及相应的现代 ID/s,然后使用 ZoneId.of(String, Map<String,String>).

一种可能的自动化翻译尝试是遍历可用时区(您通过 TimeZone.getAvailableIDs()TImeZone.getTimeZone(String) 获得)并使用 hasSameRules() 与每个时区进行比较。然后从 ID 字符串或从中获得的 TimeZone 创建您的 ZoneId

抱歉,这不是您想要的答案。

首先,当你这样做时:

SimpleTimeZone stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, 1, 1, Calendar.FEBRUARY, 1, 1, 1, 1 * 60 * 60 * 1000);

您正在创建 ID 等于 "GMT" 的时区。当您调用 toZoneId() 时,它只会调用 ZoneId.of("GMT")(它使用与参数相同的 ID,如 中所述)。然后 ZoneId class 加载 JVM 中配置的任何夏令时信息(它不保留与原始 SimpleTimeZone 对象相同的规则)。

并且根据ZoneId javadoc如果区域ID等于'GMT'、'UTC'或'UT' 那么结果是具有相同 ID 和规则的 ZoneId 等同于 ZoneOffset.UTCZoneOffset.UTC 根本没有 DST 规则。

因此,如果您想拥有一个具有相同 DST 规则的 ZoneId 实例,您必须手动创建它们(我不知道这是可能的,但实际上是这样,检查下面)。


您的 DST 规则

查看SimpleTimeZone javadoc,您创建的实例具有以下规则(根据我的测试):

  • 标准偏移量是 +02:00(提前 2 小时 UTC/GMT)
  • 夏令时从 1 月的第一个星期日开始(查看 javadoc 了解更多详细信息),午夜后 1 毫秒(您传递了 1 作为开始和结束时间)
  • 在 DST 中,偏移量更改为 +03:00
  • DST 在 2 月的第一个星期日结束,午夜后 1 毫秒(然后偏移量返回 +02:00

实际上,根据 javadoc,您应该在 dayOfWeek 参数中传递一个负数才能以这种方式工作,因此时区应该像这样创建:

stz = new SimpleTimeZone(2 * 60 * 60 * 1000, "GMT", Calendar.JANUARY, 1, -Calendar.SUNDAY, 1, Calendar.FEBRUARY, 1, -Calendar.SUNDAY, 1, 1 * 60 * 60 * 1000);

但在我的测试中,两者的工作方式相同(也许它修复了非负值)。无论如何,我已经做了一些测试来检查这些规则。首先,我使用您的自定义时区创建了一个 SimpleDateFormat

TimeZone t = TimeZone.getTimeZone("America/Sao_Paulo");
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss Z");
sdf.setTimeZone(t);

然后我测试了边界日期(夏令时开始和结束之前):

// starts at 01/01/2017 (first Sunday of January)
ZonedDateTime z = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(2));
// 01/01/2017 00:00:00 +0200 (not in DST yet, offset is still +02)
System.out.println(sdf.format(new Date(z.toInstant().toEpochMilli())));
// 01/01/2017 01:01:00 +0300 (DST starts, offset changed to +03)
System.out.println(sdf.format(new Date(z.plusMinutes(1).toInstant().toEpochMilli())));

// ends at 05/02/2017 (first Sunday of February)
z = ZonedDateTime.of(2017, 2, 5, 0, 0, 0, 0, ZoneOffset.ofHours(3));
// 05/02/2017 00:00:00 +0300 (in DST yet, offset is still +03)
System.out.println(sdf.format(new Date(z.toInstant().toEpochMilli())));
// 04/02/2017 23:01:00 +0200 (DST ends, offset changed to +02 - clock moves back 1 hour: from midnight to 11 PM of previous day)
System.out.println(sdf.format(new Date(z.plusMinutes(1).toInstant().toEpochMilli())));

输出为:

01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
05/02/2017 00:00:00 +0300
04/02/2017 23:01:00 +0200

因此,它遵循上述规则(在 01/01/2017 午夜,偏移量为 +0200,一分钟后它在夏令时(偏移量现在为 +0300;相反发生在05/02(夏令时结束,偏移量返回 +0200))。


使用上述规则创建 ZoneId

不幸的是,您无法扩展 ZoneIdZoneOffset,并且您也无法更改它们,因为它们都是不可变的。但是可以创建自定义规则并将它们分配给新的 ZoneId.

并且似乎没有办法直接将规则从 SimpleTimeZone 导出到 ZoneId,因此您必须手动创建它们。

首先我们需要创建一个 ZoneRules,一个 class,其中包含偏移量何时以及如何变化的所有规则。为了创建它,我们需要构建一个包含 2 classes:

的列表
  • ZoneOffsetTransition:定义偏移量更改的特定日期。必须至少有一个才能使其工作(空列表失败)
  • ZoneOffsetTransitionRule:定义一个通用规则,不受特定日期的限制(如"first Sunday of January the offset changes from X to Y")。我们必须有 2 个规则(一个用于 DST 开始,另一个用于 DST 结束)

那么,让我们创建它们:

// offsets (standard and DST)
ZoneOffset standardOffset = ZoneOffset.ofHours(2);
ZoneOffset dstOffset = ZoneOffset.ofHours(3);

// you need to create at least one transition (using a date in the very past to not interfere with the transition rules)
LocalDateTime startDate = LocalDateTime.MIN;
LocalDateTime endDate = LocalDateTime.MIN.plusDays(1);
// DST transitions (date when it happens, offset before and offset after) - you need to create at least one
ZoneOffsetTransition start = ZoneOffsetTransition.of(startDate, standardOffset, dstOffset);
ZoneOffsetTransition end = ZoneOffsetTransition.of(endDate, dstOffset, standardOffset);
// create list of transitions (to be used in ZoneRules creation)
List<ZoneOffsetTransition> transitions = Arrays.asList(start, end);

// a time to represent the first millisecond after midnight
LocalTime firstMillisecond = LocalTime.of(0, 0, 0, 1000000);
// DST start rule: first Sunday of January, 1 millisecond after midnight
ZoneOffsetTransitionRule startRule = ZoneOffsetTransitionRule.of(Month.JANUARY, 1, DayOfWeek.SUNDAY, firstMillisecond, false, TimeDefinition.WALL,
    standardOffset, standardOffset, dstOffset);
// DST end rule: first Sunday of February, 1 millisecond after midnight
ZoneOffsetTransitionRule endRule = ZoneOffsetTransitionRule.of(Month.FEBRUARY, 1, DayOfWeek.SUNDAY, firstMillisecond, false, TimeDefinition.WALL,
    standardOffset, dstOffset, standardOffset);
// list of transition rules
List<ZoneOffsetTransitionRule> transitionRules = Arrays.asList(startRule, endRule);

// create the ZoneRules instance (it'll be set on the timezone)
ZoneRules rules = ZoneRules.of(start.getOffsetAfter(), end.getOffsetAfter(), transitions, transitions, transitionRules);

我无法创建在午夜后第一毫秒开始的 ZoneOffsetTransition(它们实际上恰好在午夜开始),因为秒的小数部分必须为零(如果不是,ZoneOffsetTransition.of() 抛出异常)。因此,我决定将日期定在过去 (LocalDateTime.MIN),以免干扰规则。

但是 ZoneOffsetTransitionRule 实例完全按照预期工作(DST 在午夜后 1 毫秒开始和结束,就像 SimpleTimeZone 实例一样)。

现在我们必须将此 ZoneRules 设置为时区。正如我所说,ZoneId 不能扩展(构造函数不是 public),ZoneOffset 也不能(它是 final class)。我最初认为设置规则的唯一方法是创建一个实例并使用反射设置它,但实际上 API 提供了一种创建自定义 ZoneId 的方法 通过扩展 java.time.zone.ZoneRulesProvider class:

// new provider for my custom zone id's
public class CustomZoneRulesProvider extends ZoneRulesProvider {

    @Override
    protected Set<String> provideZoneIds() {
        // returns only one ID
        return Collections.singleton("MyNewTimezone");
    }

    @Override
    protected ZoneRules provideRules(String zoneId, boolean forCaching) {
        // returns the ZoneRules for the custom timezone
        if ("MyNewTimezone".equals(zoneId)) {
            ZoneRules rules = // create the ZoneRules as above
            return rules;
        }
        return null;
    }

    // returns a map with the ZoneRules, check javadoc for more details
    @Override
    protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
        TreeMap<String, ZoneRules> map = new TreeMap<>();
        ZoneRules rules = getRules(zoneId, false);
        if (rules != null) {
            map.put(zoneId, rules);
        }
        return map;
    }
}

请记住,您不应将 ID 设置为 "GMT"、"UTC" 或任何有效的 ID(您可以使用 ZoneId.getAvailableZoneIds() 检查所有存在的 ID)。 "GMT" 和 "UTC" 是 API 内部使用的特殊名称,它可能导致意外行为。所以选择一个不存在的名称——我选择了 MyNewTimezone(没有 space 否则它会失败,因为如果 space 中有 ZoneRegion 会抛出异常名字)。

让我们测试一下这个新时区。新的class必须使用ZoneRulesProvider.registerProvider方法注册:

// register the new zonerules provider
ZoneRulesProvider.registerProvider(new CustomZoneRulesProvider());
// create my custom zone
ZoneId customZone = ZoneId.of("MyNewTimezone");

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss Z");
// starts at 01/01/2017 (first Sunday of January)
ZonedDateTime z = ZonedDateTime.of(2017, 1, 1, 0, 0, 0, 0, customZone);
// 01/01/2017 00:00:00 +0200 (not in DST yet, offset is still +02)
System.out.println(z.format(fmt));
// 01/01/2017 01:01:00 +0300 (DST starts, offset changed to +03)
System.out.println(z.plusMinutes(1).format(fmt));

// ends at 05/02/2017 (first Sunday of February)
z = ZonedDateTime.of(2017, 2, 5, 0, 0, 0, 0, customZone);
// 05/02/2017 00:00:00 +0300 (in DST yet, offset is still +03)
System.out.println(z.format(fmt));
// 04/02/2017 23:01:00 +0200 (DST ends, offset changed to +02 - clock moves back 1 hour: from midnight to 11 PM of previous day)
System.out.println(z.plusMinutes(1).format(fmt));

输出相同(因此 SimpleTimeZone 使用的规则相同):

01/01/2017 00:00:00 +0200
01/01/2017 01:01:00 +0300
05/02/2017 00:00:00 +0300
04/02/2017 23:01:00 +0200


备注:

  • CustomZoneRulesProvider 只创建一个新的 ZoneId,当然您可以扩展它以创建更多。 Check the javadoc 有关如何正确实施您自己的规则提供程序的更多详细信息。
  • 在创建 ZoneRules 之前,您必须准确检查自定义时区的规则。一种方法是使用 SimpleTimeZone.toString() 方法(即 returns 对象的内部状态)并阅读 javadoc 以了解参数如何影响规则。
  • 我还没有测试足够多的案例来了解是否有某些特定日期 SimpleTimeZoneZoneId 规则以不同的方式运行。我测试了不同年份的一些日期,它似乎工作正常。

这是我更喜欢的解决方案,因为它很简单,但是您需要一个日期和一个时区作为参数来检索 ZoneId

private ZoneId getZoneOffsetFor(final Date date, final TimeZone timeZone){
  int offsetInMillis = getOffsetInMillis(date, timeZone);
  return ZoneOffset.ofTotalSeconds( offsetInMillis / 1000 );
}

private int getOffsetInMillis(final Date date, final TimeZone timeZone){
  int offsetInMillis = timeZone.getRawOffset();
  if(timeZone.inDaylightTime(date)){
     offsetInMillis += timeZone.getDSTSavings();
  }
  return offsetInMillis;
}