1

我正在逐行处理文件。

每行都有一个格式如下的日期: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 Calendar 进行轮班,但在添加日期时遇到了很多问题。我认为问题在于它正在尝试纠正 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,因为我也一直在研究这一点。任何帮助将不胜感激,谢谢。

4

2 回答 2

3

世界标准时间

始终使用UTC进行数据交换和此类读数日志。始终、始终、始终使用 UTC。将 UTC 视为一个真实时间,将其他时区视为与 UTC 的偏差。

顺便说一句,夏令时 (DST)并不是分区日期时间值唯一需要担心的问题。其他异常发生在不同的时区,导致挂钟时间向前或向后移动。解决方案很简单:避开所有时区——使用 UTC

Instant

Instant课程以纳秒级的分辨率在 UTC 时间线上捕获一个时刻。这应该是你去上课的日期时间工作。

Instant instant = Instant.now();

字符串

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

java.time 类包括Instant在解析/生成字符串时默认使用 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

日期:2014-11-02T01:15

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

瞬间:2014-11-02T05:15:00Z

……还有……</p>

输入:141102 0215

日期:2014-11-02T02:15

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

瞬间:2014-11-02T07:15:00Z

您可以在 IdeOne.com 中看到此代码。请注意 UTC 值的两小时差异 ( Z)。

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

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

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

您可以玩猜谜游戏来尝试调整这些设计不佳的琴弦。如果您确定在每个时钟小时内至少有一个样本,您可以跟踪以前的样本并查看正在处理的样本是否具有早于前一个的时钟时间。如果是这样,您可以假设这是 DST 后备切换,并添加一个小时plusHours( 1 )

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

但这是一个冒险的解决方案。我不确定这项工作,因为我没有考虑清楚。如果绝望,也许你可以让它工作。

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

于 2016-11-03T03:16:24.383 回答
2

夏令时 世界上许多国家都采用所谓的夏令时 (DST),即在夏季将时钟提前一小时的做法(好吧,并非所有国家都在夏季,但让我们忍一忍吧)节省时间开始。

当白天时间结束时,时钟将拨回一小时。这样做是为了更好地利用自然光。

ZonedDateTime 完全了解 DST。

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

2015年,意大利夏令时于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 边界的时间时,我们还需要小心。这是因为两者对夏令时的处理方式不同。

考虑在意大利夏令时开始前一小时:

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

当我们添加一天的 Duration 时:

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]

原因是 Period 添加了一个概念日期,而 Duration 恰好添加了一天(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 规则的。

但是,它完全不知道 Zones 和 Offsets,这就是为什么我们需要将其转换为完全 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,而 [Europe/Rome] 是 ZoneId。

让我们通过增加十分钟来触发夏令时:

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);
于 2020-03-07T11:18:28.823 回答