【问题标题】:Getting ZoneId from a SimpleTimeZone从 SimpleTimeZone 获取 ZoneId
【发布时间】:2017-11-07 22:26:05
【问题描述】:

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

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

实际上,toZoneId 返回一个没有夏令时信息的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();

【问题讨论】:

  • 请查看 Javadoc。 TimeZone 有一个 toZoneId() 方法。
  • 如果我使用这种方法,我会得到一个不受支持的区域 ID 异常,因为他们使用自定义 ID 构建 TimeZone
  • 如果我将 ID 更改为“GMT”,则返回的 ZoneId 不包含夏令时信息
  • 你能提供一个失败的例子吗?在您的问题中。
  • 您在对SimpleTimeZone 的构造函数调用中指定的参数值似乎不遵守该类的约定。请再次阅读课程说明。您的代码看起来不正确。通常,您设置为1 的至少一个参数应该是负数或零。并且将时间参数(以毫秒为单位的时间)设置为 1 至少可以说很奇怪。一旦你明确了你的参数值,你就可以去下一步构造一个ZoneRules的特殊实例(这是一个非常复杂的步骤)。

标签: java timezone dst java-time


【解决方案1】:

首先,当你这样做时:

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,正如@Ole V.V.'s answer 中所述)。然后ZoneId 类加载 JVM 中配置的任何夏令时信息(它与原始 SimpleTimeZone 对象的规则不同)。

根据ZoneId javadoc如果区域 ID 等于 'GMT'、'UTC' 或 'UT',则结果是具有相同 ID 和规则的 ZoneId ZoneOffset.UTC。而ZoneOffset.UTC 根本没有夏令时规则。

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


您的 DST 规则

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

  • 标准偏移量为+02:00(提前 2 小时 UTC/GMT)
  • DST 从 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);

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

// 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())));

输出是:

2017 年 1 月 1 日 00:00:00 +0200
2017 年 1 月 1 日 01:01:00 +0300
05/02/2017 00:00:00 +0300
2017 年 4 月 2 日 23:01:00 +0200

因此,它遵循上述规则(在 01/01/2017 午夜,偏移量为 +0200,一分钟后它在 DST 中(偏移量现在为 +0300;相反的情况发生在 05/02(DST 结束并且偏移量回到+0200))。


使用上述规则创建ZoneId

很遗憾,您不能扩展 ZoneIdZoneOffset,也不能更改它们,因为它们都是不可变的。但是可以创建自定义规则并将它们分配给新的ZoneId

而且它似乎没有办法直接将规则从SimpleTimeZone 导出到ZoneId,所以你必须手动创建它们。

首先我们需要创建一个ZoneRules,一个包含偏移量何时以及如何改变的所有规则的类。为了创建它,我们需要构建一个包含 2 个类的列表:

  • ZoneOffsetTransition:定义偏移更改的特定日期。必须至少有一个才能使其工作(空列表失败)
  • ZoneOffsetTransitionRule:定义一般规则,不限于特定日期(如“1 月的第一个星期日偏移量从 X 变为 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 不能扩展(构造函数不是公共的),ZoneOffset 也不能扩展(它是一个final 类)。我最初认为设置规则的唯一方法是创建一个实例并使用反射设置它,但实际上API 提供了一种通过扩展java.time.zone.ZoneRulesProvider 来创建自定义ZoneId 的方法类:

// 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(没有空格,否则它会失败,因为如果名称中有空格,ZoneRegion 会抛出异常)。

让我们测试一下这个新时区。必须使用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使用的规则是一样的):

2017 年 1 月 1 日 00:00:00 +0200
2017 年 1 月 1 日 01:01:00 +0300
05/02/2017 00:00:00 +0300
2017 年 4 月 2 日 23:01:00 +0200


注意事项:

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

【讨论】:

  • 我首先在想“什么应该阻止我们创建自己的 ZoneId 子类?”但是我们需要调用超类构造函数(隐式或显式),ZoneId 的唯一构造函数是包私有(默认可见性)。所以我们显然不应该这样做(我们至少必须在同一个包中声明我们自己的类,真是一团糟)。
  • 哇,答案真棒!但这似乎是对 java.time API 的完整破解
  • @Woody 我已经更新了答案 - 我找到了一种创建自定义时区的方法不使用反射。因此,它不再是 hack,因为使用javadoc 中设计和解释的公共 API。我希望它现在可以解决您的问题。
  • @OleV.V.我终于找到了一种方法。 API 提供了一种方法来创建我们自己的 ZoneId's - 间接(通过扩展 ZoneRulesProvider),但确实如此。
  • @Hugo 不错的更新,但似乎我必须使用规则预先配置自定义 zoneId,但我们的规则是动态的,所以我不能使用你的建议或者我错过了什么?
【解决方案2】:

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

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

所以我想你最好的办法是找出你的旧代码试图给你的时区的正确时区 ID,然后从 ZoneId.of(String) 获取你的 ZoneId。或者更好一点,构建您自己的别名映射,其中包含旧代码中的一个或多个 ID 和相应的现代 ID/s,然后使用 ZoneId.of(String, Map&lt;String,String&gt;)

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

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

【讨论】:

  • 我找到了一种方法来解决我的问题,而无需在系统中设置好的 ID。实际上,可能无法找到匹配的 ID,因为我们提供了夏令时和 gmt 偏移的完全自定义
【解决方案3】:

这是我更喜欢的解决方案,因为它很简单,但您需要一个日期和一个时区作为参数来检索 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;
}

【讨论】:

  • 你可以达到与getZoneOffsetFor 相同的结果timeZone.toZoneId().getRules().getOffset(date.toInstant())。您可以使用相同的偏移量来设置clonedTimeZone ID:clonedTimeZone.setID("GMT" + zoneOffset.getTotalSeconds() / 3600)。但是您代码中的结果 ID 是GMTGMT+xxx,(格林威治标准时间两次),这是正确的吗?而且您不会将clonedTimeZone 用于任何事情。无论如何,我很高兴你找到了解决方案,因为我不清楚你是否想这样做。
  • 谢谢,确实不需要 clonedTimeZone
猜你喜欢
  • 1970-01-01
  • 2019-12-07
  • 1970-01-01
  • 1970-01-01
  • 2021-10-25
  • 2020-04-19
  • 1970-01-01
  • 2012-10-22
  • 2019-04-11
相关资源
最近更新 更多