在dst

时间:2016-11-02 20:06:55

标签: java jodatime java.util.calendar

我正在逐行处理文件。

每一行都有以下格式的日期:YYMMDD HHMM

该文件基本上每15分钟记录一次。录音使用日光节省了它所在的时区。我遇到的问题是在春季前进和后退。记录是重复发生后退的日期和发生后退时的间隙。

后备示例:

141102 0100

141102 0115

141102 0130

141102 0145

141102 0200

141102 0115 - 重复

141102 0130 - 重复

141102 0145 - 重复

141102 0200 - 重复

春季前进示例:

150308 0200

150308 0315

我需要做的是,在夏令时期间将所有日期提前一小时。

我正在处理的程序目前正在使用java.util.Calendar。我已经尝试使用java日历进行转换,并且在添加日期时遇到了很多问题。我假设问题是它正试图纠正DST问题本身,当我想要做的就是将日期改为一小时。它也存在检测差距的问题。例如,它认为第一个1:15 - 2:00是在白天,而不是这种情况。

基本上我想要的是将后备示例更改为此日期范围内的所有内容,以便向后移动一小时:

后备示例:

141102 0100无变化

141102 0115无变化

141102 0130无变化

141102 0145无变化

141102 0200无变化

141102 0115至141102 0215

141102 0130至141102 0230

141102 0145至141102 0245

141102 0200至141102 0300

继续改变日期,直到它迎来春天

150308 0200至150308 0300

150308 0315没有变化。

我尝试了很多方法,似乎无法找到解决方案。我并不反对使用Joda Time,因为我也在考虑这一点。任何帮助将不胜感激,谢谢。

2 个答案:

答案 0 :(得分:2)

UTC

始终使用UTC进行数据交换和此类读数日志。始终始终使用UTC。将UTC视为 One True Time ,将其他时区视为与UTC的偏差。

顺便说一下,Daylight Saving Time (DST) 唯一需要担心的是分区日期时间值。其他异常发生在各个时区,导致wall-clock time向前或向后移动。解决方案很简单:避免使用所有时区 - 使用UTC

Instant

Instant类在UTC时间轴上捕获片刻,分辨率为纳秒。这应该是你上课的日期时间工作。

Instant instant = Instant.now();

字符串

对于日志记录,请生成标准ISO 8601格式的字符串。坚持使用经过验证的ISO 8601格式,而不是发明自己的格式。

在解析/生成字符串时,默认情况下包含Instant的java.time类使用ISO 8601格式。因此无需指定格式化模式。

String output = instant.toString();
  

2016-11-02T21:10:05.321Z

Instant instant = Instant.parse( "2016-11-02T21:10:05.321Z" );

并且始终在您的日期中包含世纪20。省略只会为错误解释中的错误创造大量机会。存储和内存真的很便宜,我们可以承受两个额外的数字。

时区指示器

始终在序列化日期时间值中包含时区或偏离UTC指示符。

在上面显示的Instant::toString结果中,Z代表Zulu,表示UTC。

解析字符串

以下是解析日期时间字符串的代码。我们首先解析为LocalDateTime,没有任何时区或偏移,因为您的输入缺少时区或偏移的任何指示。然后我们分配一个时区来获得ZonedDateTime,看看它如何处理DST切换的时刻。

我认为你所讨论的时区使用了凌晨2点的DST回落切换。我使用America/Montreal作为例子。

List<String> inputs = new ArrayList<>( 2 );
inputs.add( "141102 0115" );  // 1 AM
inputs.add( "141102 0215" );  // 2 AM

for( String input : inputs ) {
    DateTimeFormatter f = DateTimeFormatter.ofPattern( "uuMMdd HHmm" );
    LocalDateTime ldt = LocalDateTime.parse( input , f );

    // At 2 AM in this zone, the clock falls back one hour for DST cutover. The 1 AM hour repeats.
    ZoneId z = ZoneId.of( "America/Montreal" );
    ZonedDateTime zdt = ldt.atZone( z );

    System.out.println( "input: " + input );
    System.out.println( "ldt: " + ldt );
    System.out.println( "zdt: " + zdt );
    System.out.println( "instant: " + zdt.toInstant() );
    System.out.println("");
}
  

输入:141102 0115

     

ldt:2014-11-02T01:15

     

zdt:2014-11-02T01:15-04:00 [美国/蒙特利尔]

     

时刻:2014-11-02T05:15:00Z

...和...

  

输入:141102 0215

     

ldt:2014-11-02T02:15

     

zdt:2014-11-02T02:15-05:00 [美国/蒙特利尔]

     

时刻:2014-11-02T07:15:00Z

你可以see this code live in IdeOne.com。请注意UTC值的两小时差异(Z)。

我们看到的行为已经记录在案。

  

在大多数情况下,本地日期时间只有一个有效偏移量。在重叠的情况下,时钟被设置回来,有两个有效的偏移。此方法使用通常对应于“summer”的早期偏移量。

如果1:15 AM不明确,可能是第一次或第二次出现,java.time与第一次出现。

你可以玩一个猜谜游戏来尝试调整这些设计糟糕的字符串。如果您确定在每个时钟小时内至少有一个样本,则可以跟踪先前的样本并查看正在处理的样本是否具有比之前更早的时钟时间。如果是这样,您可以假设这是DST后退切换,并使用plusHours( 1 )添加一小时。

if( processingZdt.toLocalDateTime().isBefore( previousZdt.toLocalDateTime() ) { 
    processingZdt.plusHours( 1 ) ;  // A hack; I am *not* recommending this.
    …

但这是一个有风险的解决方案。我不确定这项工作,因为我没想过。如果绝望,也许你可以让它发挥作用。

更明智的方法是预防:使用UTC

答案 1 :(得分:1)

夏令时 世界上许多国家/地区采用所谓的夏令时(DST),这种做法是在夏令时将夏令时提前一个小时(当然,并非所有国家都在夏令时)。开始。

夏令时结束时,时钟将倒退一小时。这样做是为了更好地利用自然光。

ZonedDateTime完全了解DST。

例如,让我们举一个完全遵守DST的国家,例如意大利(UTC / GMT +2)。

2015年,DST于3月29日在意大利开始,并于10月25日结束。这意味着:

March, 29 2015 at 2:00:00 A.M. clocks were turned forward 1 hour to
March, 29 2015 at 3:00:00 A.M. local daylight time instead
(So a time like March, 29 2015 2:30:00 A.M. didn't actually exist!)

October, 25 2015 at 3:00:00 A.M. clocks were turned backward 1 hour to
October, 25 2015 at 2:00:00 A.M. local daylight time instead
(So a time like October, 25 2015 2:30:00 A.M. actually existed twice!)

如果我们使用此日期/时间创建LocalDateTime实例并进行打印:

LocalDateTime ldt = LocalDateTime.of(2015, 3, 29, 2, 30);
System.out.println(ldt);

结果将是:

2015-03-29T02:30

但是,如果我们为意大利创建ZonedDateTime实例(请注意格式使用的是城市,而不是国家/地区)并打印:

ZonedDateTime zdt = ZonedDateTime.of(
     2015, 3, 29, 2, 30, 0, 0, ZoneId.of("Europe/Rome"));
System.out.println(zdt);

使用DST时,结果将与现实世界中的结果一样

2015-03-29T03:30+02:00[Europe/Rome]

但是要小心。我们必须使用区域性ZoneId,ZoneOffset不能解决问题,因为此类没有用于DST的区域规则信息:

ZonedDateTime zdt1 = ZonedDateTime.of(
     2015, 3, 29, 2, 30, 0, 0, ZoneOffset.ofHours(2));
ZonedDateTime zdt2 = ZonedDateTime.of(
     2015, 3, 29, 2, 30, 0, 0, ZoneId.of("UTC+2"));
System.out.println(zdt1); System.out.println(zdt2);

结果将是:

2015-03-29T02:30+02:00[UTC+02:00] 2015-03-29T02:30+02:00

当我们为意大利创建ZonedDateTime实例时,我们必须添加一个小时才能看到效果(否则,我们将在新时间的3:00创建ZonedDateTime):

ZonedDateTime zdt = ZonedDateTime.of(
    2015, 10, 25, 2, 30, 0, 0, ZoneId.of("Europe/Rome"));
ZonedDateTime zdt2 = zdt.plusHours(1);
System.out.println(zdt);
System.out.println(zdt2);

结果将是:

2015-10-25T02:30+02:00[Europe/Rome] 2015-10-25T02:30+01:00[Europe/Rome]

当使用采用TemporalAmount实现的plus()和minus()方法版本(即,Period或Duration)来调整DST边界上的时间时,我们还需要小心。这是因为两者对夏令时的处理方式不同。

在意大利DST开始前一小时考虑一下:

ZonedDateTime zdt = ZonedDateTime.of(
    2015, 3, 29, 1, 0, 0, 0, ZoneId.of("Europe/Rome"));

当我们添加一天的持续时间时:

System.out.println(zdt.plus(Duration.ofDays(1)));

结果是:

2015-03-30T02:00+02:00[Europe/Rome]

当我们添加为期一天的时间时:

System.out.println(zdt.plus(Period.ofDays(1)));

结果是:

2015-03-30T01:00+02:00[Europe/Rome]

原因是“期间”添加了一个概念性的日期,而“持续时间”恰好添加了一天(24小时或86、400秒),当它超过DST边界时,添加了一个小时,而最终时间为02:00的01:00。

================================================ ======================== Java 8引入了java.time.LocalDateTime:

LocalDateTime localDateTimeBeforeDST = LocalDateTime
  .of(2018, 3, 25, 1, 55);

assertThat(localDateTimeBeforeDST.toString())
  .isEqualTo("2018-03-25T01:55");

我们可以观察到LocalDateTime如何符合ISO-8601规则。

但是,它完全不知道区域和偏移量,这就是为什么我们需要将其转换为完全具备DST意识的java.time.ZonedDateTime:

ZoneId italianZoneId = ZoneId.of("Europe/Rome");
ZonedDateTime zonedDateTimeBeforeDST = localDateTimeBeforeDST
  .atZone(italianZoneId);

assertThat(zonedDateTimeBeforeDST.toString())
  .isEqualTo("2018-03-25T01:55+01:00[Europe/Rome]"); 

我们可以看到,日期现在包含两个基本的尾随信息:+01:00是ZoneOffset,而[欧洲/罗马]是ZoneId。

通过增加十分钟来触发DST:

ZonedDateTime zonedDateTimeAfterDST = zonedDateTimeBeforeDST
  .plus(10, ChronoUnit.MINUTES);

assertThat(zonedDateTimeAfterDST.toString())
  .isEqualTo("2018-03-25T03:05+02:00[Europe/Rome]");

同样,我们看到时间和区域偏移如何向前移动,并且仍然保持相同的距离:

Long deltaBetweenDatesInMinutes = ChronoUnit.MINUTES
  .between(zonedDateTimeBeforeDST,zonedDateTimeAfterDST);
assertThat(deltaBetweenDatesInMinutes)
  .isEqualTo(10);