Joda-Time、夏令时更改和日期时间解析

Posted

技术标签:

【中文标题】Joda-Time、夏令时更改和日期时间解析【英文标题】:Joda-Time, Daylight Saving Time change and date time parsing 【发布时间】:2011-04-29 12:19:30 【问题描述】:

我在使用Joda-Time 解析和生成Daylight Saving Time (DST) 小时左右的日期和时间时遇到以下问题。这是一个示例(请注意,2008 年 3 月 30 日是意大利的夏令时变化):

DateTimeFormatter dtf = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss");
DateTime x = dtf.parseDateTime("30/03/2008 03:00:00");
int h = x.getHourOfDay();
System.out.println(h);
System.out.println(x.toString("dd/MM/yyyy HH:mm:ss"));
DateTime y = x.toDateMidnight().toDateTime().plusHours(h);
System.out.println(y.getHourOfDay());
System.out.println(y.toString("dd/MM/yyyy HH:mm:ss"));

我得到以下输出:

3
30/03/2008 03:00:00
4
30/03/2008 04:00:00

当我解析小时时,我得到小时是 3。在我的数据结构中,我保存了存储午夜时间的日期,然后我为一天中的每个小时 (0-23) 设置了一些值。然后,当我写出日期时,我重新计算完整的日期时间,使午夜加小时。当我将 3 小时加到午夜时,我得到 04:00:00!如果我再次解析它,我会得到 4 小时!

我的错误在哪里?有什么方法可以在我解析时获得第 2 小时,或者在我打印输出时获得第 3 小时?

我也尝试过手动构建输出:

String.format("%s %02d:00:00", date.toString("dd/MM/yyyy"), h);

但在这种情况下,对于第 2 小时,我生成 30/03/2008 02:00:00 这不是有效日期(因为第 2 小时不存在)并且无法再解析。

提前感谢您的帮助。 菲利波

【问题讨论】:

仅供参考,Joda-Time 项目现在位于maintenance mode,团队建议迁移到java.time 类。见Tutorial by Oracle。 【参考方案1】:

当我将 3 小时加到午夜时,我得到 04:00:00!如果我再次解析它,我会得到 4 小时!我的错在哪里?

您已经提到,这个日期正是时间变化的时间。所以没有错误。 2010 年 3 月 30 日 00:00 CEST(意大利的时区)准确地说是 2010 年 3 月 29 日 23:00 UTC。添加 3 小时后,您将获得 2010 年 3 月 30 日 02:00 UTC。但这是我们切换时间的时刻(发生在 01:00 UTC),所以当您将时间转换为本地时区时,您会得到 3 月 30 日 04:00。这是正确的行为。

有什么方法可以在我解析时获得第 2 小时,或者在我打印输出时获得第 3 小时?

不,因为 2010 年 3 月 30 日 02:00 CEST 不存在。恰好在 2010 年 3 月 30 日 01:00 UTC,我们将时间从 +1 小时切换到 +2 小时而不是 UTC,因此 2010 年 3 月 30 日 00:59 UTC 是 2010 年 3 月 30 日:01:59 CEST,但 2010 年 3 月 30 日 01 :00 UTC 变为 2010 年 3 月 30 日 03:00 CEST。该特定日期不存在 02:xx 小时。

顺便说一句。在一周内,您可以期待另一个“乐趣”。你能说出这是指UTC中的哪个日期吗:

2010 年 10 月 31 日 02:15 CEST ?

好吧,有趣的是,我们不知道。它可以是 2010 年 10 月 31 日 00:15 UTC(实际时间切换之前)或 2010 年 10 月 31 日 01:15 UTC(切换之后)。

这正是您应该始终存储与 UTC 相关的日期和时间并在显示之前将它们转换为本地时区的原因,否则您可能会产生歧义。

HTH。

【讨论】:

+1 用于在不存储与 UTC 相关的日期时澄清问题。【参考方案2】:

您保存数据的数据结构在夏令时的日子里并不是很理想。您在这一天的一天应该只有 23 小时。

如果你这样做:

    DateTimeFormatter dtf = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss").withLocale(Locale.US);
    DateTime x = dtf.parseDateTime("30/03/2008 00:00:00");

    DateTimeFormatter parser = DateTimeFormat.fullDateTime();
    System.out.println("Start:"+parser.print(x));

    DateTime y = x.plusHours(4);

    System.out.println("After add of 4:"+parser.print(y));

你得到了预期的结果,时间是 05:00。

我建议您更改存储日期的方式并使用日期。如果不是,您必须在存储一天中的小时时处理夏令时。

你可以这样做: 在我们将时间向前移动一小时的情况下,像这种情况,您必须存储 4 而不是 5 作为 5 的时间。并且在计算时间时,您应该使用 plusHours() 方法获取实际时间。我认为你可能会逃脱类似的事情:

public class DateTest 
    private static final int HOUR_TO_TEST = 2;

  public static void main(String[] args) 
    DateTimeFormatter dtf = DateTimeFormat.forPattern("dd/MM/yyyy HH:mm:ss");
    DateTime startOfDay = dtf.parseDateTime("30/03/2008 00:00:00");

    /* Obtained from new DateTime() in code in practice */
    DateTime actualTimeWhenStoring = startOfDay.plusHours(HOUR_TO_TEST);

    int hourOfDay = actualTimeWhenStoring.getHourOfDay();
    int hourOffset = startOfDay.plusHours(hourOfDay).getHourOfDay();

    System.out.println("Hour of day:" + hourOfDay);
    System.out.println("Offset hour:" + hourOffset);

    int timeToSave = hourOfDay;
    if (hourOffset != hourOfDay) 
        timeToSave = (hourOfDay + (hourOfDay - hourOffset));
    
    System.out.println("Time to save:" + timeToSave);

    /* When obtaining from db: */
    DateTime recalculatedTime = startOfDay.plusHours(timeToSave);

    System.out.println("Hour of time 'read' from db:" + recalculatedTime.getHourOfDay());
  

...或者基本上类似的东西。如果你选择走这条路,我会为它写一个测试。您可以更改 HOUR_TO_TEST 以查看它是否已超过夏令时。

【讨论】:

感谢您的回复。遗憾的是,当我存储没有小时的数据(您的 HOUR_TO_TEST 属性)时,我从我解析的日期时间字符串中获取它。所以我唯一的解决方案(我认为)是每小时检查一次 dst 变化,并在需要时进行更正。我现在正在从这个意义上修改我的代码,它似乎可以工作。 如果你有一个字符串,我猜你可以使用 hourOfDay 作为那个,对吧?提供字符串的人是在某个时区提供的,对吗?如果那个时区也受到夏令时的影响,那么这应该不是问题,因为生产者和作为消费者的你在同一个时区(那么非法日期 02:00 将永远不会发送给你)。如果您获取其他时区的日期,例如 UTC,则 parseDateTime 方法具有解析当前语言环境以外的其他时区的功能 - 请查阅文档。【参考方案3】:

以 Paweł Dyda 和 Knubo 的正确答案为基础……

ISO 8601 字符串格式

您不应该将 (serialize) 日期时间存储为您提到的格式的字符串:"30/03/2008 03:00:00"。问题:

省略时区。 日、月、年顺序不明确。 应该已转换为 UTC 时间。

如果您必须将日期时间值序列化为文本,请使用可靠的格式。显而易见的选择是ISO 8601 标准格式。更好的是将本地时间转换为UTC (Zulu) 时区,然后转换为 ISO 8601 格式。像这样:2013-11-01T04:48:53.044Z

没有午夜

Joda-Time 中的 midnight 方法已被弃用,取而代之的是 Joda-Time 方法 withTimeAtStartOfDay()(请参阅 doc)。有些日子会not have a midnight。

Joda-Time 2.3 中的示例代码

关于这个源码的一些cmets:

    // © 2013 Basil Bourque. This source code may be used freely forevery by anyone taking full responsibility for doing so.

    // Joda-Time - The popular alternative to Sun/Oracle's notoriously bad date, time, and calendar classes bundled with Java 7 and earlier.
    // http://www.joda.org/joda-time/

    // Joda-Time will become outmoded by the JSR 310 Date and Time API introduced in Java 8.
    // JSR 310 was inspired by Joda-Time but is not directly based on it.
    // http://jcp.org/en/jsr/detail?id=310

    // By default, Joda-Time produces strings in the standard ISO 8601 format.
    // https://en.wikipedia.org/wiki/ISO_8601

示例显示意大利罗马的 DST(夏令时)当天为 23 小时,而后一天为 24 小时。请注意,时区(罗马)已指定。

    // Time Zone list: http://joda-time.sourceforge.net/timezones.html
    org.joda.time.DateTimeZone romeTimeZone = org.joda.time.DateTimeZone.forID("Europe/Rome");
    org.joda.time.DateTime dayOfDstChange = new org.joda.time.DateTime( 2008, 3, 30, 0, 0, romeTimeZone ) ; // Day when DST
    org.joda.time.DateTime dayAfter = dayOfDstChange.plusDays(1);

    // How many hours in this day? Should be 23 rather than 24 on day of Daylight Saving Time "springing ahead" to lose one hour.
    org.joda.time.Hours hoursObjectForDay = org.joda.time.Hours.hoursBetween(dayOfDstChange.withTimeAtStartOfDay(), dayAfter.withTimeAtStartOfDay());
    System.out.println( "Expect 23 hours, got: " + hoursObjectForDay.getHours() ); // Extract an int from object.

    // What time is 3 hours after midnight on day of DST change?
    org.joda.time.DateTime threeHoursAfterMidnightOnDayOfDst = dayOfDstChange.withTimeAtStartOfDay().plusHours(3);
    System.out.println( "Expect 4 AM (04:00) for threeHoursAfterMidnightOnDayOfDst: " + threeHoursAfterMidnightOnDayOfDst );

    // What time is 3 hours after midnight on day _after_ DST change?
    org.joda.time.DateTime threeHoursAfterMidnightOnDayAfterDst = dayAfter.withTimeAtStartOfDay().plusHours(3);
    System.out.println( "Expect 3 AM (03:00) for threeHoursAfterMidnightOnDayAfterDst: " + threeHoursAfterMidnightOnDayAfterDst );

通过首先转换为 UTC 来存储日期时间的示例。然后在恢复日期时间对象后,调整到所需的时区。

    // Serialize DateTime object to text.
    org.joda.time.DateTimeZone romeTimeZone = org.joda.time.DateTimeZone.forID("Europe/Rome");
    org.joda.time.DateTime dayOfDstChangeAtThreeHoursAfterMidnight = new org.joda.time.DateTime( 2008, 3, 30, 0, 0, romeTimeZone ).withTimeAtStartOfDay().plusHours(3);
    System.out.println("dayOfDstChangeAtThreeHoursAfterMidnight: " + dayOfDstChangeAtThreeHoursAfterMidnight);
    // Usually best to first change to UTC (Zulu) time when serializing.
    String dateTimeSerialized = dayOfDstChangeAtThreeHoursAfterMidnight.toDateTime( org.joda.time.DateTimeZone.UTC ).toString();
    System.out.println( "dateTimeBeingSerialized: " + dateTimeSerialized );
    // Restore
    org.joda.time.DateTime restoredDateTime = org.joda.time.DateTime.parse( dateTimeSerialized );
    System.out.println( "restoredDateTime: " + restoredDateTime );
    // Adjust to Rome Italy time zone.
    org.joda.time.DateTime restoredDateTimeAdjustedToRomeItaly = restoredDateTime.toDateTime(romeTimeZone);
    System.out.println( "restoredDateTimeAdjustedToRomeItaly: " + restoredDateTimeAdjustedToRomeItaly );

运行时:

dayOfDstChangeAtThreeHoursAfterMidnight: 2008-03-30T04:00:00.000+02:00
dateTimeBeingSerialized: 2008-03-30T02:00:00.000Z
restoredDateTime: 2008-03-30T02:00:00.000Z
restoredDateTimeAdjustedToRomeItaly: 2008-03-30T04:00:00.000+02:00

【讨论】:

以上是关于Joda-Time、夏令时更改和日期时间解析的主要内容,如果未能解决你的问题,请参考以下文章

将日期字符串解析为某个时区(支持夏令时)

Joda-Time,夏令时计算,时区独立测试

如果日期是进行 DST(夏令时)更改的实际日期,有没有办法在 Python 中推断?

夏令时持续时间

Ruby/Rails 中的夏令时开始和结束日期

ColdFusion 2018和三个Char夏令时代码的BlazeDS DateTime解析错误