使用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();

最佳答案

首先,当您这样做时:

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(UTC / GMT提前2小时)
DST从一月的第一个星期日开始(有关更多详细信息,请参阅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
  01/01/2017 01:01:00 +0300
  2017/05/02 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:定义了不受特定日期限制的一般规则(例如“一月的第一个星期日,偏移量从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设置为“ 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
  01/01/2017 01:01:00 +0300
  2017/05/02 00:00:00 +0300
  2017年4月2日23:01:00 +0200




笔记:


CustomZoneRulesProvider仅创建一个新的ZoneId,但是您当然可以扩展它以创建更多的ZoneRulesCheck the javadoc有关如何正确实施自己的规则提供程序的更多详细信息。
创建SimpleTimeZone.toString()之前,必须先检查自定义时区中的规则是什么。一种方法是使用SimpleTimeZone方法(该方法返回对象的内部状态)并读取javadoc来了解参数如何影响规则。
我还没有测试足够多的情况来知道ZoneId和规则是否在某些特定日期表现出不同的方式。我已经测试了不同年份的一些日期,它似乎运行良好。

10-02 06:09
查看更多