【问题标题】:DateUtils#truncate() to work in UTC timezoneDateUtils#truncate() 在 UTC 时区工作
【发布时间】:2015-02-01 19:41:35
【问题描述】:

我有一个 java.util.Date,我使用 org.apache.commons.lang.time#truncate(javaUtilDateObj, Calendar.DATE) 将日期转换为午夜。

问题是 UTC 日期以本地时区表示,而 truncate 方法只是将小时、分钟和秒转换为 00:00:00,而不考虑时区。

示例:- 假设UTC时间是从纪元开始的30秒。 Wed Dec 31 19:00:30 EST 1969 是我看到的 Date 对象表示。在上述日期调用 DateUtils#truncate() 方法时,输出为 Wed Dec 31 00:00:00 EST 1969。

我所期待的是,如果 UTC 时间距离纪元 30 秒。如果这可以表示/转换为 Thu Jan 1 00:00:30 EST 1970,我可以调用 DateUtils#truncate() 方法并期待 Thu Jan 1 00:00:00 EST 1970。

注意:我无法使用 joda-time API,因此我无法使用现有的 API。

【问题讨论】:

  • 仅供参考,除了 UTC,由于夏令时 (DST) 和可能的其他异常情况,午夜并不总是 00:00:00.000 的时间。为此,Joda-Time 提供了方法withTimeAtStartOfDay()

标签: java date datetime apache-commons-dateutils


【解决方案1】:

使用Calendar 版本的truncate

Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();

【讨论】:

  • setTime() zwraca void a nie Calendar, dodałem poprawne rozwiązanie
【解决方案2】:
Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
utc.setTime(date);
Calendar midnight = DateUtils.truncate(utc, Calendar.HOUR_OF_DAY);
Date midnightDate = midnight.getTime();

【讨论】:

    【解决方案3】:

    tl;博士

    • 使用现代 java.time 类。内置于 Java 中。
    • 在某些地方的某些日期,这一天可能从 00:00:00 开始。
      • java.time 确定一天中的第一个时刻。
    • 避免使用模棱两可的术语“午夜”。
      • 想想“一天的第一刻”。
      • 搜索 Stack Overflow 以了解半开时间跨度。

    一天的第一刻:

    ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;    // Specify the region whose wall-clock time you want to perceive “today”. Specify time zone with `Continent/Region`, never the 3-4 letter pseudo-zones such as `EST` or `IST`. 
    LocalDate today = LocalDate.now( z ) ;          // Capture the current date as seen in a specific time zone.
    ZonedDateTime zdt = today.atStartOfDay( z ) ;   // Let java.time determine first moment of the day.
    Instant instant = zdt.toInstant() ;             // Adjust from time zone to UTC. Same moment, same point on the timeline, different wall-clock time.
    

    详情

    这里有很多问题。

    调整区域

    如果 UTC 时间距离纪元 30 秒。如果可以表示/转换为 Thu Jan 1 00:00:30 EST 1970,

    不,错了。你不能转换。这两个日期时间值代表同一时刻。如果EST 指的是America/New_YorkAmerica/Montreal 等时区,那么这两个值相差几个小时

    Instant thirtySecondsFromEpoch = Instant.EPOCH.plusSeconds( 30 ) ;
    

    thirtySecondsFromEpoch.toString(): 1970-01-01T00:00:30Z

    在北美东海岸的大部分地区看到同样的时刻。

    ZoneId z = ZoneId.of( "America/New_York" ) ;
    ZonedDateTime zdtAdjustedFromThirtySecondsFromEpoch = thirtySecondsFromEpoch.atZone( z ) ;
    

    zdtAdjustedFromThirtySecondsFromEpoch.toString(): 1969-12-31T19:00:30-05:00[America/New_York]

    因为北美东海岸的大多数人使用的挂钟时间比世界标准时间晚 5 小时,所以过去 30 秒的同一时刻被视为过去半分钟前一天晚上 7 点,1969 年的最后一天。

    尝试相同的时间,即 1970 年第一天 30 秒后,但以 America/New_York 时区而不是 UTC 时间显示。现在我们正在谈论一个完全不同的时刻。新一天的半分钟发生在五个小时后比 UTC 的新一天的半分钟。

    ZonedDateTime zdt = ZonedDateTime.of( 1970 , 1 , 1 , 0 , 0 , 30 , 0 , z ) ;
    

    zdt.toString(): 1970-01-01T00:00:30-05:00[美国/纽约]

    zdt.toInstant().toString(): 1970-01-01T05:00:30Z

    时区

    您忽略了时区的关键问题。

    时区对于确定日期至关重要。对于任何给定的时刻,日期在全球范围内因区域而异。例如,Paris France 中午夜后几分钟是新的一天,而 Montréal Québec 中仍然是“昨天”。

    continent/region 的格式指定proper time zone name,例如America/MontrealAfrica/CasablancaPacific/Auckland。切勿使用 3-4 个字母的缩写,例如 ESTIST,因为它们不是真正的时区,没有标准化,甚至不是唯一的 (!)。

    java.util.Date::toString

    Date 上的 toString 方法有一个善意但麻烦的行为,即在生成表示 Date 对象的 UTC 值的字符串时隐式应用 JVM 的当前默认时区。造成此区域存在于Date 中的错误印象。

    这是避免这个糟糕课程的众多原因之一。请改用 java.time 类。

    java.time

    现代方法使用 java.time 类来代替麻烦的旧旧日期时间类,例如 Date

    如果您手头有Date,请通过添加到旧类的新方法转换为java.time.Instant

    Instant instant = myJavaUtilDate.toInstant() ;
    

    避免谈论“午夜”,因为这是一个无定形的话题。相反,请专注于一天的第一刻,即一天的开始。

    不要假设一天从 00:00:00 开始。夏令时 (DST) 等异常情况意味着一天可能从一天中的另一个时间开始,例如 01:00:00。让 java.time 确定一天的开始。

    确定一天的开始意味着确定日期。如上所述,确定日期需要时区。因此,我们需要将您在 UTC 中的 Instant 调整为特定时区中的 ZonedDateTime (ZoneId)。

    ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
    ZonedDateTime zdt = instant.atZone( z ) ;
    

    ZonedDateTime 中提取仅包含日期的部分。

    LocalDate ld = zdt.toLocalDate() ;
    

    现在在指定时区询问一天中的第一刻。

    ZonedDateTime zdtStartOfDay = ld.atStartOfDay( z ) ;
    

    截断

    您可以截断各种 java.time 对象。查找truncatedTo 方法,在该方法中传递TemporalUnit 对象(可能是ChronoUnit)以指定结果中所需的粒度。为此目的不需要 Apache DateUtils。

    如果截断 Instant,您始终使用 UTC 作为时区逻辑。

    Instant instantTrucToDay = Instant.now().truncatedTo( ChronoUnit.DAYS ) ;
    

    您更有可能希望在上下文中截断除 UTC 之外的某个时区,如上所述。

    ZonedDateTime zdtTruncToDay = zdt.truncatedTo( ChronoUnit.DAYS ) ;
    

    顺便说一句,如果您的最终目标是使用整个日期,即没有任何时间或区域的仅日期,请坚持使用LocalDate

    如果 UTC 时间距离纪元 30 秒。如果可以表示/转换为 Thu Jan 1 00:00:30 EST 1970

    不确定你在这里的意思。但请记住,在 1970-01-01T00:00:00Z (1970-01-01T00:00:30Z) 的纪元之后 30 秒,北美东海岸的同一时刻要早几个小时。这意味着前一天晚上 7 点左右。

    Instant instantThirtySecsAfterEpoch = Instant.EPOCH.plusSeconds( 30 ) ;
    

    instantThirtySecsAfterEpoch.toString(): 1970-01-01T00:00:30Z

    ZoneId z = ZoneId.of( "America/New_York" ) ;
    ZonedDateTime zdt = instantThirtySecsAfterEpoch.atZone( z ) ;
    

    zdt.toString(): 1969-12-31T19:00:30-05:00[美国/纽约]


    关于java.time

    java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧 legacy 日期时间类,例如 java.util.DateCalendarSimpleDateFormat

    Joda-Time 项目现在位于maintenance mode,建议迁移到java.time 类。

    要了解更多信息,请参阅Oracle Tutorial。并在 Stack Overflow 上搜索许多示例和解释。规格为JSR 310

    使用符合JDBC 4.2 或更高版本的JDBC driver,您可以直接与您的数据库交换java.time 对象。不需要字符串或 java.sql.* 类。

    从哪里获得 java.time 类?

    ThreeTen-Extra 项目通过附加类扩展了 java.time。该项目是未来可能添加到 java.time 的试验场。您可以在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

    【讨论】:

      【解决方案4】:

      如果您的数据层保存 UTC 时间但您的表示层有不同的时区并且您需要截断日期进行过滤,则以下功能很有用。

      /**
       * Truncates the given UTC date for the given TimeZone
       *
       * @param date UTC date
       * @param timeZone Target timezone
       * @param field Calendar field
       *
       * @return
       */
      public static Date truncate(Date date, final TimeZone timeZone, final int field)
      {
          int timeZoneOffset = timeZone.getOffset(date.getTime());
      
          // convert UTC date to target timeZone
          date = DateUtils.addToDate(date, Calendar.MILLISECOND, timeZoneOffset);
      
          // truncate in target TimeZone
          date = org.apache.commons.lang3.time.DateUtils.truncate(date, field);
      
          // convert back to UTC
          date = DateUtils.addToDate(date, Calendar.MILLISECOND, -timeZoneOffset);
      
          return date;
      }
      
      /**
       * Adds the given amount to the given Calendar field to the given date.
       *
       * @see Calendar
       *
       * @param date the date
       * @param field the calendar field to add to
       * @param amount the amount to add, may be negative
       *
       * @return
       */
      public static Date addToDate(final Date date, final int field, final int amount)
      {
          Objects.requireNonNull(date, "date must not be null");
      
          final Calendar c = Calendar.getInstance();
          c.setTime(date);
          c.add(field, amount);
          return c.getTime();
      }
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-01-24
      • 2020-02-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-23
      • 1970-01-01
      相关资源
      最近更新 更多