正如@Misha的回答中已经解释的那样,这是由于夏令时规则引起的。
在圣保罗,夏令时从2015年10月18日午夜开始:时钟向前移动1个小时,因此它从23:59:59
"跳"到01:00:00
。 在00:00:00
和00:59:59
之间存在差距,因此时间00:30
会相应调整。
您可以使用ZoneRules
和ZoneOffsetTransition
类来检查日期和时间是否适用于该时区:
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());
System.out.println(validOffsets.get(0));
对于洛杉矶时区,返回1个有效偏移量:
-07:00
。
PS:偏移量的更改通常是由于夏令时造成的,但并非总是如此。夏令时和偏移量由政府和法律定义,它们可以随时更改。因此,有效偏移量中的间隙也可能意味着发生了这种变化(某些政治家决定更改国家的标准偏移量,因此间隙可能与夏令时无关)。
您还可以检查更改发生的时间以及更改前后的偏移量:
ZoneId sp = ZoneId.of("America/Sao_Paulo");
ZoneRules rules = sp.getRules();
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();
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();
ZoneOffsetTransitionRule tr = rules.getTransitionRules().get(1);
ZoneOffsetTransition t = tr.createTransition(2015);
另一种检查日期和时间是否适用于某个时区的方法是使用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));
System.out.println(ZonedDateTime.ofStrict(dt, ZoneOffset.ofHours(-3), sp));
第一个案例是没问题的,因为在给定的日期/时间内,
-7
的偏移量对于洛杉矶是有效的。第二个案例会抛出异常,因为在给定的日期/时间内,
-3
的偏移量对于圣保罗是无效的。