从SimpleTimeZone获取ZoneId

时间:2017-06-06 19:01:04

标签: java timezone dst java-time

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

我想检索ZoneId以便能够使用Java 8时间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();

2 个答案:

答案 0 :(得分:5)

首先,当你这样做时:

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规则。

所以,如果你想拥有一个具有相同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);

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

// 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,一分钟后它在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提供了一种创建自定义ZoneId 的方法扩展java.time.zone.ZoneRulesProvider类:

// 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设置为&#34; GMT&#34;,&#34; UTC&#34;或任何有效ID(您可以使用{{检查所有现有ID) 1}})。 &#34; GMT&#34;和&#34; UTC&#34;是API内部使用的特殊名称,它可能导致意外行为。所以选择一个不存在的名称 - 我选择了ZoneId.getAvailableZoneIds()(没有空格,否则它会失败,因为如果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使用的相同):

  

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()方法(返回对象的内部状态)并阅读javadoc以了解参数如何影响规则。
  • 我还没有对足够多的案例进行测试,以了解SimpleTimeZoneZoneId规则是否存在某种特定日期以不同的方式表现。我已经测试了不同年份的某些日期,似乎工作正常。

答案 1 :(得分:1)

我不认为这是可能的。人们也可能会问它会有什么意义。如果时区不在Olson数据库中,那么它在现实世界中很难使用,那么它在你的程序中有什么用处?当然,我理解您的旧程序创建SimpleTimeZone 代表野外使用的时区的情况,但只是给它一个不正确的ID,以便{{1}无法做出正确的翻译。

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

因此,我认为您最好的办法是找出旧版代码试图为您提供的时区的正确时区ID,然后从ZoneId获取ZoneId。或稍微好一点,构建您自己的别名地图,其中包含遗留代码中的ID或ID以及相应的现代ID,然后使用ZoneId.of(String)

自动化翻译的一种可能尝试是迭代可用时区(您通过ZoneId.of(String, Map<String,String>)TimeZone.getAvailableIDs()获得)并使用TImeZone.getTimeZone(String)进行比较。然后从ID字符串或从中获取的hasSameRules()创建ZoneId

对不起,这不是你所希望的答案。