ZonedDateTime在夏令时使用冬季的同一时区吗?

7
ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
    ZoneId.of("America/Sao_Paulo")); 
System.out.println(zdt); // 2015-10-18T01:30-02:00[America/Sao_Paulo]

当我们设置小时为0时,你会发现显示的小时为1,时区为UTC-02:00,但夏令时应该是UTC-03:00

下面是另一个例子:

ZonedDateTime zdt = ZonedDateTime.of(2015, 10, 18, 0, 30, 0, 0,
    ZoneId.of("America/Los_Angeles"));
System.out.println(zdt); //2015-10-18T00:30-07:00[America/Los_Angeles]

你可以看到夏令时时区是 UTC-07:00,小时数为 0,这是我们设置的结果。
它们为什么不同呢?
2个回答

8
这是因为您选择的时间恰好落在巴西切换夏令时时期的午夜和01:00之间的间隙中。那个时间实际上是不可能的,所以您会得到文档中描述的行为:

在间隙中,即时钟向前跳跃时,不存在有效的偏移量。相反,本地日期时间将被调整为晚于间隙长度。对于典型的一小时夏令时更改,本地日期时间将向后移动一小时,进入通常对应于“夏季”的偏移量。

您可以通过在三月对应的夜晚02:00和03:00之间选择一个时间来观察洛杉矶时区的相同行为。
zdt = ZonedDateTime.of(2015, 3, 8, 2, 30, 0, 0,
        ZoneId.of("America/Los_Angeles"));
System.out.println(zdt); 

4

正如@Misha的回答中已经解释的那样,这是由于夏令时规则引起的。

在圣保罗,夏令时从2015年10月18日午夜开始:时钟向前移动1个小时,因此它从23:59:59 "跳"到01:00:00。 在00:00:0000:59:59之间存在差距,因此时间00:30会相应调整。

您可以使用ZoneRulesZoneOffsetTransition类来检查日期和时间是否适用于该时区:

ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();
// check if 2015-10-18 00:30 is valid for this timezone
LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30);
List<ZoneOffset> validOffsets = rules.getValidOffsets(dt);
System.out.println(validOffsets.size()); // size is zero, no valid offsets at 00:30

getValidOffsets方法 返回指定日期/时间的所有有效偏移量。如果列表为空,则表示该时区中不存在该日期/时间(通常是因为夏令时,时钟向前跳跃)。

当日期/时间存在于时区中时,会返回一个偏移量:

ZoneId la = ZoneId.of("America/Los_Angeles");
rules = la.getRules();
validOffsets = rules.getValidOffsets(dt);
System.out.println(validOffsets.size()); // 1 - date/time valid for this timezone
System.out.println(validOffsets.get(0)); // -07:00

对于洛杉矶时区,返回1个有效偏移量:-07:00
PS:偏移量的更改通常是由于夏令时造成的,但并非总是如此。夏令时和偏移量由政府和法律定义,它们可以随时更改。因此,有效偏移量中的间隙也可能意味着发生了这种变化(某些政治家决定更改国家的标准偏移量,因此间隙可能与夏令时无关)。
您还可以检查更改发生的时间以及更改前后的偏移量:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();

// get the previous transition (the last one that occurred before 2015-10-18 00:30 in Sao_Paulo timezone 
ZoneOffsetTransition t = rules.previousTransition(dt.atZone(sp).toInstant());
System.out.println(t);

输出结果为:

转换时间[2015-10-18T00:00-03:00到-02:00之间有断层]

这意味着在2015-10-18T00:00处有一个断层(时钟向前移动),偏移量将从-03:00更改为-02:00(因此,时钟向前移动1个小时)。
您也可以单独获取所有这些信息:
System.out.println(t.getDateTimeBefore() + " -> " + t.getDateTimeAfter());
System.out.println(t.getOffsetBefore() + " -> " + t.getOffsetAfter());

输出结果为:

2015-10-18T00:00 -> 2015-10-18T01:00
-03:00 -> -02:00

它表明,在 00:00 时钟直接跳到 01:00(因此 00:30 不存在)。在第二行中,是变化前后的偏移量。
如果您检查 洛杉矶 时区的转换,您会发现它的夏令时开始和结束日期不同:
ZoneId la = ZoneId.of("America/Los_Angeles");
rules = la.getRules();

// 2015-10-18 00:30 in Los Angeles
Instant instant = dt.atZone(la).toInstant();
System.out.println(rules.previousTransition(instant));
System.out.println(rules.nextTransition(instant));

输出结果为:

过渡[2015年3月8日02:00-08:00到-07:00的间隙]
过渡[2015年11月1日02:00-07:00到-08:00的重叠]

因此,在洛杉矶时区,夏令时从2015年3月8日开始,到2015年11月1日结束。这就是为什么在2015年10月18日,所有小时都是有效的(与圣保罗时区不同,那里会进行调整)。
有些时区有转换规则(例如“夏令时于十月第三个星期日开始”),而不仅仅是转换时间(例如“夏令时在这个具体日期和时间开始”),如果可用,您也可以使用它们。
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();

// hardcoded: Sao_Paulo timezone has 2 transition rules, the second one is relative to October
// but you should always check if the list is not empty
ZoneOffsetTransitionRule tr = rules.getTransitionRules().get(1);
// get the transition for year 2015
ZoneOffsetTransition t = tr.createTransition(2015);
// use t the same way as above (the output will be the same)

另一种检查日期和时间是否适用于某个时区的方法是使用ZonedDateTime.ofStrict方法,如果日期和时间对于时区无效,则会抛出异常:

ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneId la = ZoneId.of("America/Los_Angeles");
LocalDateTime dt = LocalDateTime.of(2015, 10, 18, 0, 30);

System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-7), la)); // OK
System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-3), sp)); // throws java.time.DateTimeException

第一个案例是没问题的,因为在给定的日期/时间内,-7 的偏移量对于洛杉矶是有效的。第二个案例会抛出异常,因为在给定的日期/时间内,-3 的偏移量对于圣保罗是无效的。

非常感谢,@Hugo。 - Hao Ma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接