使用LocalDate类计算指定天数的日期差异

ant*_*009 12 java threetenbp localdate

我正在使用openjdk版本1.8.0_112-release进行开发,但是也需要支持以前的JDK版本(Java-8之前版本) - 因此无法使用java.time.

我正在编写一个简单的类来计算日期,以查看保存的日期是否在当前日期之前,这意味着它已过期.

但是,我不确定我是否以正确的方式做到了这一点.我正在LocalDate上课来计算天数.从用户单击保存的日期和时间开始计算到期时间.该日期将被保存,并将根据此保存的日期和时间以及当前日期和时间(即用户登录时)进行检查.

这是最好的方法吗?我想继续LocalDate上课.

import org.threeten.bp.LocalDate;

public final class Utilities {
    private Utilities() {}

    public static boolean hasDateExpired(int days, LocalDate savedDate, LocalDate currentDate) {
        boolean hasExpired = false;

        if(savedDate != null && currentDate != null) {
            /* has expired if the saved date plus the specified days is still before the current date (today) */
            if(savedDate.plusDays(days).isBefore(currentDate)) {
                hasExpired = true;
            }       
        }

        return hasExpired;
    }
}
Run Code Online (Sandbox Code Playgroud)

我正在使用这样的类:

private void showDialogToIndicateLicenseHasExpired() {
    final LocalDate currentDate = LocalDate.now();
    final int DAYS_TO_EXPIRE = 3;
    final LocalDate savedDate = user.getSavedDate();

    if(hasDateExpired(DAYS_TO_EXPIRE, savedDate, currentDate)) {
        /* License has expired, warn the user */    
    }
}
Run Code Online (Sandbox Code Playgroud)

我正在寻找一个考虑时区的解决方案.如果许可证设置为在3天后过期,则用户将前往不同的时区.即他们可以根据小时数提前或退出.许可证仍应过期.

Ole*_*.V. 10

你的代码基本上没问题.我会基本上以同样的方式做到这一点,只是一两个细节不同.

正如Hugo已经指出的那样,我会使用java.time.LocalDate并放弃使用ThreeTen Backport(除非特定要求您的代码也可以在Java 6或7上运行).

时区

您应该决定在哪个时区计算您的日期.如果你在你的代码中明确时区,我更愿意.如果您的系统仅在您自己的时区使用,则选择很简单,只需明确说明即可.例如:

    final LocalDate currentDate = LocalDate.now(ZoneId.of("Asia/Hong_Kong"));
Run Code Online (Sandbox Code Playgroud)

请填写相关的区域ID.即使有一天它恰好在具有不正确时区设置的计算机上运行,​​这也将确保程序正常工作.如果您的系统是全局的,您可能希望使用UTC,例如:

    final LocalDate currentDate = LocalDate.now(ZoneOffset.UTC);
Run Code Online (Sandbox Code Playgroud)

当用户单击"保存"以保存日期时,您将需要执行类似操作,以便您的数据保持一致.

72小时

编辑:我从您的评论中了解到,您希望从保存时间开始测量3天,即72小时,以确定许可证是否已过期.为此,a LocalDate没有给你足够的信息.这是一个没有时间的日期,如公元2017年5月26日.还有一些其他选择:

  • Instant是一个时间点(具有纳秒精度,甚至).无论用户是否移动到另一个时区,这都是确保在72小时后到期的简单解决方案.
  • ZonedDateTime代表日期,时间和时区,如2017年5月29日公布19:21:33.783,偏移GMT + 08:00 [亚洲/香港].如果要在保存的时间内提醒用户,ZonedDateTime您将允许您使用计算保存日期的时区显示该信息.
  • 最后OffsetDateTime也会起作用,但它似乎没有给你很多其他的优点,所以我不会详细说明这个选项.

由于瞬时在所有时区都相同,因此在获取当前时刻时不指定时区:

    final Instant currentDate = Instant.now();
Run Code Online (Sandbox Code Playgroud)

添加3天Instant是一个有点不同LocalDate,但其余的逻辑是相同的:

public static boolean hasDateExpired(int days, Instant savedDate, Instant currentDate) {
    boolean hasExpired = false;

    if(savedDate != null && currentDate != null) {
        /* has expired if the saved date plus the specified days is still before the current date (today) */
        if (savedDate.plus(days, ChronoUnit.DAYS).isBefore(currentDate)) {
            hasExpired = true;
        }       
    }

    return hasExpired;
}
Run Code Online (Sandbox Code Playgroud)

使用的ZonedDateTime,而另一方面,那张酷似LocalDate中的代码:

    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.of("Asia/Hong_Kong"));
Run Code Online (Sandbox Code Playgroud)

如果要从运行程序的JVM中设置当前时区:

    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.systemDefault());
Run Code Online (Sandbox Code Playgroud)

现在,如果你宣布public static boolean hasDateExpired(int days, ZonedDateTime savedDate, ZonedDateTime currentDate),你可以像以前一样:

        /* has expired if the saved date plus the specified days is still before the current date (today) */
        if (savedDate.plusDays(days).isBefore(currentDate)) {
            hasExpired = true;
        }       
Run Code Online (Sandbox Code Playgroud)

即使两个ZonedDateTime对象位于两个不同的时区,这也将执行正确的比较.因此,无论用户是否前往不同的时区,他/她都不会在许可证到期前获得更少或更多的时间.


小智 8

您可以使用ChronoUnit.DAYS(在org.threeten.bp.temporal包中,或者在java.time.temporal使用java 8本机类时)来计算两个LocalDate对象之间的天数:

if (savedDate != null && currentDate != null) {
    if (ChronoUnit.DAYS.between(savedDate, currentDate) > days) {
        hasExpired = true;
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑(赏金后解释)

对于此测试,我使用的是threetenbp版本1.3.4

由于您希望解决方案即使用户位于不同的时区也能正常工作,因此您不应该使用LocalDate,因为此类不处理时区问题.

我认为最好的解决方案是使用该Instant课程.它代表了一个单一的时间点,无论你在什么时区(此时,世界上的每个人都在同一时刻,尽管当地的日期和时间可能因你所处的位置而异).

实际上Instant总是在UTC时间 - 时区的标准独立,因此非常适合您的情况(因为您希望计算独立于用户所在的时区).

所以你savedDate和你的currentDate必须是Instant,你应该计算它们之间的差异.

现在,一个微妙的细节.您希望在3天后过期.对于我做的代码,我做了以下假设:

  • 3天= 72小时
  • 72小时后1分之一秒,它已过期

第二个假设对我实施解决方案的方式很重要.我正在考虑以下情况:

  1. currentDate不到72小时后savedDate- 未过期
  2. currentDate正好是72小时后savedDate- 未过期(或过期?见下面的评论)
  3. currentDate超过72小时后savedDate(即使只有几分之一秒) - 已过期

Instant班已纳秒的精度,这样的情况下,3我在考虑,它的即使是1纳秒72小时后过期:

import org.threeten.bp.Instant;
import org.threeten.bp.temporal.ChronoUnit;

public static boolean hasDateExpired(int days, Instant savedDate, Instant currentDate) {
    boolean hasExpired = false;

    if (savedDate != null && currentDate != null) {
        // nanoseconds between savedDate and currentDate > number of nanoseconds in the specified number of days
        if (ChronoUnit.NANOS.between(savedDate, currentDate) > days * ChronoUnit.DAYS.getDuration().toNanos()) {
            hasExpired = true;
        }
    }

    return hasExpired;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我曾经ChronoUnit.DAYS.getDuration().toNanos()在一天内获得纳秒数.最好依靠API而不是硬编码容易出错的数字.

我做了一些测试,使用相同时区和不同时区的日期.我用used ZonedDateTime.toInstant()方法将日期转换为Instant:

import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;

// testing in the same timezone
ZoneId sp = ZoneId.of("America/Sao_Paulo");
// savedDate: 22/05/2017 10:00 in Sao Paulo timezone
Instant savedDate = ZonedDateTime.of(2017, 5, 22, 10, 0, 0, 0, sp).toInstant();
// 1 nanosecond before expires (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 9, 59, 59, 999999999, sp).toInstant()));
// exactly 3 days (72 hours) after saved date (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 10, 0, 0, 0, sp).toInstant()));
// 1 nanosecond after 3 days (72 hours) (returns true - expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 10, 0, 0, 1, sp).toInstant()));

// testing in different timezones (savedDate in Sao Paulo, currentDate in London)
ZoneId london = ZoneId.of("Europe/London");
// In 22/05/2017, London will be in summer time, so 10h in Sao Paulo = 14h in London
// 1 nanosecond before expires (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 13, 59, 59, 999999999, london).toInstant()));
// exactly 3 days (72 hours) after saved date (returns false - not expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 14, 0, 0, 0, london).toInstant()));
// 1 nanosecond after 3 days (72 hours) (returns true - expired)
System.out.println(hasDateExpired(3, savedDate, ZonedDateTime.of(2017, 5, 25, 14, 0, 0, 1, london).toInstant()));
Run Code Online (Sandbox Code Playgroud)

PS:对于案例2 (currentDate正好在保存日期后72小时 - 未过期) - 如果您希望此过期,只需更改if以上内容即可使用>=而不是>:

if (ChronoUnit.NANOS.between(savedDate, currentDate) >= days * ChronoUnit.DAYS.getDuration().toNanos()) {
    ... // it returns "true" for case 2
}
Run Code Online (Sandbox Code Playgroud)

如果您不想要纳秒精度并且只想比较日期之间的天数,您可以像@Ole VV的答案那样进行.我相信我们的答案非常相似(我怀疑代码是相同的,虽然我不确定),但我没有测试足够的情况来检查它们在任何特定情况下是否有所不同.


Bas*_*que 5

Hugo的答案和Ole VV的答案都是正确的,Ole VV的答案是最重要的,因为时区对于确定当前日期至关重要.

Period

这项工作的另一个有用的课程是Period班级.此类表示未附加到时间线的时间跨度,包括年,月和日.

请注意,此类适合表示此课程所需的已用时间,因为此表示形式为"分块"为年,然后是几个月,然后是任何剩余天数.因此,如果LocalDate.between( start , stop )使用了几个星期,结果可能是"两个月和三天".请注意,由于这个原因,该类没有实现Comparable接口,因为除非我们知道涉及哪个特定月份,否则不能说一对月份比另一对更大或更小.

我们可以使用此类来表示问题中提到的两天宽限期.这样做可以使我们的代码更加自我记录.最好传递这种类型的对象,而不是传递一个整数.

Period grace = Period.ofDays( 2 ) ;

LocalDate start = LocalDate.of( 2017 , Month.JANUARY , 23 ).plusDays( grace ) ;
LocalDate stop = LocalDate.of( 2017 , Month.MARCH , 7 ) ;
Run Code Online (Sandbox Code Playgroud)

我们ChronoUnit用来计算经过的天数.

int days = ChronoUnit.DAYS.between( start , stop ) ;
Run Code Online (Sandbox Code Playgroud)

Duration

顺便说一下,这个Duration类类似于Period它代表了一个没有附加到时间轴的时间跨度.但Duration代表总共秒数加上以纳秒为单位的小数秒.从中您可以计算出一些通用的24小时工作日(不是基于日期的天数),小时数,分钟数,秒数和小数秒数.请记住,日子并不总是24小时; 在美国,由于夏令时,它们目前可能长达23,24或25小时.

这个问题是关于基于日期的日子,而不是24小时的问题.所以这个Duration课程不合适.


关于java.time

java.time框架是建立在Java 8和更高版本.这些类取代麻烦的老传统日期时间类,如java.util.Date,Calendar,和SimpleDateFormat.

现在处于维护模式Joda-Time项目建议迁移到java.time类.

要了解更多信息,请参阅Oracle教程.并搜索Stack Overflow以获取许多示例和解释.规范是JSR 310.

从哪里获取java.time类?

ThreeTen-额外项目与其他类扩展java.time.该项目是未来可能添加到java.time的试验场.您可以在此比如找到一些有用的类Interval,YearWeek,YearQuarter,和更多.

  • @ ant2009那么你不应该说你在问题的第一行使用了Java 8. (2认同)