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

Calcuting the date difference for a specified number of days using LocalDate class

我正在使用 openjdk 版本 1.8.0_112-release 进行开发,但也需要支持以前的 JDK 版本(pre-Java-8) - 所以不能使用 java.time.

我正在编写一个实用程序 class 来计算日期以查看保存的日期是否早于当前日期,这意味着它已过期。

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

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

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;
    }
}

我是这样使用 class 的:

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 */    
    }
}

我正在寻找一个将时区考虑在内的解决方案。如果许可证设置为 3 天后到期,并且用户要前往不同的时区。也就是说,他们可能会根据小时数领先或落后。许可证应该仍会过期。

您可以使用 ChronoUnit.DAYS(在 org.threeten.bp.temporal 包中,或者如果您使用 java 8 原生 classes,则在 java.time.temporal 中)来计算数字2 LocalDate 个对象之间的天数:

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

编辑(赏金解释后)

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

由于您想要一个即使用户处于不同时区也能正常工作的解决方案,因此您不应使用 LocalDate,因为此 class 无法处理时区问题。

我认为最好的解决办法是使用Instantclass。它代表一个时间点,无论你在哪个时区(此刻,世界上的每个人都在同一个 instant,尽管 local date 和时间 可能因您所在的位置而异。

实际上 Instant 总是在 UTC Time - 一个标准的 indepedent 时区,所以非常适合你的情况(因为你想要独立于用户所在的时区)。

所以你的savedDatecurrentDate一定是Instant的,你应该计算它们之间的差值。

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

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

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

  1. currentDatesavedDate 不到 72 小时 - 未过期
  2. currentDate 恰好在 savedDate 之后 72 小时 - 未过期(或已过期?请参阅下面的评论)
  3. currentDatesavedDate 晚了超过 72 小时(即使是几分之一秒)- 已过期

Instant class 具有纳秒精度,所以如果 3 我认为它已经过期,即使它在 72 小时后是 1 纳秒:

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;
}

请注意,我使用 ChronoUnit.DAYS.getDuration().toNanos() 来获取一天中的纳秒数。最好依靠 API 而不是硬编码容易出错的大数字。

我做了一些测试,使用同一时区和不同时区的日期。 我使用 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()));

PS: 对于案例 2 (currentDate 刚好是 72 小时后savedDate - not expired) - 如果你想让这个expired,只需将上面的if改成使用>= 而不是 >:

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

如果您不想要纳秒精度,而只想比较日期之间的天数,您可以按照中的方式进行操作。我相信我们的答案非常相似(我 怀疑 这些代码是等效的,尽管我不确定),但我没有测试足够的案例来检查它们是否有任何不同特殊情况。

我觉得用这个更好:

Duration.between(currentDate.atStartOfDay(), savedDate.atStartOfDay()).toDays() > days;

Duration class 放在 java.time 包中。

你的代码基本上没问题。我会以基本相同的方式来做,只是有一两个细节不同。

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

时区

您应该决定在哪个时区计算您的天数。如果您在代码中明确指定时区,我也更愿意。如果您的系统将仅在您自己的时区使用,那么选择很容易,只需明确说明即可。例如:

    final LocalDate currentDate = LocalDate.now(ZoneId.of("Asia/Hong_Kong"));

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

    final LocalDate currentDate = LocalDate.now(ZoneOffset.UTC);

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

72 小时

编辑:我从您的评论中了解到,您希望从保存时间开始计算 3 天,即 72 小时,以确定许可证是否已过期。为此,LocalDate 无法为您提供足够的信息。它只是一个没有时钟时间的日期,例如公元 2017 年 5 月 26 日。还有一些其他选项:

  • Instant 是一个时间点(甚至纳秒精度)。这是确保无论用户是否移动到另一个时区都在 72 小时后过期的简单解决方案。
  • ZonedDateTime 表示日期、时间和时区,例如 2017 年 5 月 29 日 AD 19:21:33.783,偏移量 GMT+08:00[Asia/Hong_Kong]。如果您想提醒用户保存时间是什么时候,ZonedDateTime 将允许您显示该信息以及计算保存日期的时区。
  • 最后 OffsetDateTime 也可以,但它似乎并没有给你太多其他两个的优点,所以我不会详细说明这个选项。

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

    final Instant currentDate = Instant.now();

Instant 添加 3 天有点不同 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;
}

另一方面,ZonedDateTime 的使用与代码中的 LocalDate 完全相同:

    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.of("Asia/Hong_Kong"));

如果你想从程序运行s:

所在的JVM中设置当前时区
    final ZonedDateTime currentDate = ZonedDateTime.now(ZoneId.systemDefault());

现在如果你声明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;
        }       

这将执行正确的比较,即使两个 ZonedDateTime 对象位于两个不同的时区。因此,无论用户是否前往不同的时区,s/he 在许可证到期前的小时数不会减少或增加。

雨果的回答和奥莱的回答V.V。两者都正确,并且 ,因为时区对于确定当前日期至关重要。

Period

另一个对这项工作有用的 class 是 Period class。此 class 表示未附加到时间线的时间跨度,表示为年、月和日数。

请注意,此 class 适合表示此问题所需的经过时间,因为此表示 "chunked" 为年,然后是月,然后是任何剩余的天数。因此,如果 LocalDate.between( start , stop ) 被使用了数周,结果可能类似于 "two months and three days"。请注意,由于这个原因,class 实现 Comparable 接口,因为不能说一对月份大于或小于另一对,除非我们知道涉及哪些具体个月。

我们可以用这个class来表示问题中提到的两天宽限期。这样做可以使我们的代码更加自文档化。传递一个这种类型的对象比传递一个纯粹的整数更好。

Period grace = Period.ofDays( 2 ) ;

LocalDate start = LocalDate.of( 2017 , Month.JANUARY , 23 ).plusDays( grace ) ;
LocalDate stop = LocalDate.of( 2017 , Month.MARCH , 7 ) ;

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

int days = ChronoUnit.DAYS.between( start , stop ) ;

Duration

顺便说一句,Duration class 与 Period 类似,因为它代表了一个不附加到时间轴的时间跨度。但是 Duration 表示总秒数加上以纳秒为单位解析的小数秒。由此您可以计算出一些通用的 24 小时制天数( 不是 基于日期的天数)、小时、分钟、秒和小数秒。请记住,日子并不总是 24 小时。在美国,由于夏令时,它们目前可能是 23、24 或 25 小时。

这个问题是关于基于日期的日子,而不是 24 小时的集合。所以 Duration class 在这里不合适。


关于java.time

java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

从哪里获得java.time classes?

ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

public static boolean hasDateExpired(int days, java.util.Date savedDate) {
    long expires = savedDate().getTime() + (86_400_000L * days);
    return System.currentTimeMillis() > expires;
}

在旧的 JRE 上工作得很好。 Date.getTime() 给出毫秒 UTC,因此时区甚至不是一个因素。神奇的 86'400'000 是一天中的毫秒数。

如果您只对 savedTime 使用 long,则可以进一步简化它,而不是使用 java.util.Date。

我构建了一个简单的实用程序 class ExpiredDate,带有时区(例如 CET)、expiredDate、expireDays 和 differenceInHoursMillis。

我使用 java.util.Date 和 Date.before(expiredDate):

查看Date() 乘以 expiryDays plus (时差乘以 expiryDays) 在 expiredDate.

之前

任何早于 expiredDate 的日期都是 "expired"。

通过添加 (i) + (ii) 创建一个新日期:

(i)。我使用一天中的毫秒数 (DAY_IN_MS = 1000 * 60 * 60 * 24) 乘以(数量)expireDays。

+

(ii)。为了处理不同的时区,我找到了默认时区(对我来说是 BST)和传入 ExpiredDate 的时区(例如 CET)之间的毫秒数。对于CET,相差一小时,即3600000毫秒。这是乘以(数量)expireDays。

新日期parseDate()返回。

如果 new DateexpiredDate 之前 -> 将 expired 设置为 True。 dateTimeWithExpire.before(expiredDate);

我创建了 3 个测试:

  1. 设置有效期7天,expireDays = 3

    未过期(7天大于3天)

  2. 设置到期日期/时间,expireDays为2天

    未过期 - 因为 CET 时区将两个小时 (每天一小时) 添加到 dateTimeWithExpire

  3. 设置有效期为1天,expireDays = 2(1天小于2天)

过期是真的


package com.chocksaway;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class ExpiredDate {
    /**
     * milliseconds in a day
    */
    private static final long DAY_IN_MS = 1000 * 60 * 60 * 24;
    private String timeZone;
    private Date expiredDate;
    private int expireDays;
    private int differenceInHoursMillis;

    /**
     *
     * @param timeZone - valid timezone
     * @param expiredDate - the fixed date for expiry
     * @param expireDays - the number of days to expire
    */
    private ExpiredDate(String timeZone, Date expiredDate, int expireDays) {
        this.expiredDate = expiredDate;
        this.expireDays = expireDays;
        this.timeZone = timeZone;

        long currentTime = System.currentTimeMillis();
        int zoneOffset =   TimeZone.getTimeZone(timeZone).getOffset(currentTime);
        int defaultOffset = TimeZone.getDefault().getOffset(currentTime);

        /**
         * Example:
         *     TimeZone.getTimeZone(timeZone) is BST
         *     timeZone is CET
         *
         *     There is one hours difference, which is 3600000 milliseconds
         *
         */

        this.differenceInHoursMillis = (zoneOffset - defaultOffset);
    }


    /**
     *
     * Subtract a number of expire days from the date
     *
     * @param dateTimeNow - the date and time now
     * @return - the date and time minus the number of expired days
     *           + (difference in hours for timezone * expiryDays)
     *
     */
    private Date parseDate(Date dateTimeNow) {
        return new Date(dateTimeNow.getTime() - (expireDays * DAY_IN_MS) + (this.differenceInHoursMillis * expireDays));
    }


    private boolean hasDateExpired(Date currentDate) {
        Date dateTimeWithExpire = parseDate(currentDate);

        return dateTimeWithExpire.before(expiredDate);
    }


    public static void main(String[] args) throws ParseException {

        /* Set the expiry date 7 days, and expireDays = 3
        *
        * Not expired
        */

        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -7);

        ExpiredDate expired = new ExpiredDate("CET", cal.getTime(), 3);

        Date dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

        /* Set the expiry date / time, and expireDays to 2 days
         *  Not expired - because the CET timezone adds two hours to the dateTimeWithExpire
        */


        cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -2);

        expired = new ExpiredDate("CET", cal.getTime(), 2);

        dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

        /* Set the expiry date 1 days, and expireDays = 2
        *
        * expired
        */

        cal = Calendar.getInstance();
        cal.add(Calendar.DATE, -1);

        expired = new ExpiredDate("CET", cal.getTime(), 2);

        dateTimeNow = new Date();

        if (expired.hasDateExpired(dateTimeNow)) {
            System.out.println("expired");
        } else {
            System.out.println("NOT expired");

        }

     } 
 }

由于这个问题没有得到"enough responses",我添加了另一个答案:

我使用了 "SimpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));"将时区设置为 UTC。因此不再有时区(所有日期/时间将设置为 UTC)。

savedDate 设置为 UTC。

dateTimeNow 也设置为 UTC,过期数 "days"(负数) 添加到 dateTimeNow

新日期 expiresDate 使用 dateTimeNow

中的长毫秒

检查 if savedDate.before(expiresDate)


package com.chocksaway;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

public class ExpiredDate {
    private static final long DAY_IN_MS = 1000 * 60 * 60 * 24;

    private static boolean hasDateExpired(int days, java.util.Date savedDate) throws ParseException {
        SimpleDateFormat dateFormatUtc = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
        dateFormatUtc.setTimeZone(TimeZone.getTimeZone("UTC"));

        // Local Date / time zone
        SimpleDateFormat dateFormatLocal = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");

        // Date / time in UTC
        savedDate = dateFormatLocal.parse( dateFormatUtc.format(savedDate));

        Date dateTimeNow = dateFormatLocal.parse( dateFormatUtc.format(new Date()));

        long expires = dateTimeNow.getTime() + (DAY_IN_MS * days);

        Date expiresDate = new Date(expires);

        System.out.println("savedDate \t\t" + savedDate + "\nexpiresDate \t" + expiresDate);

        return savedDate.before(expiresDate);
    }

    public static void main(String[] args) throws ParseException {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, 0);

        if (ExpiredDate.hasDateExpired(-2, cal.getTime())) {
            System.out.println("expired");
        } else {
            System.out.println("not expired");
        }

        System.out.print("\n");

        cal.add(Calendar.DATE, -3);

        if (ExpiredDate.hasDateExpired(-2, cal.getTime())) {
            System.out.println("expired");
        } else {
            System.out.println("not expired");
        }
    }
}

运行 此代码给出以下输出:

savedDate       Mon Jun 05 15:03:24 BST 2017
expiresDate     Sat Jun 03 15:03:24 BST 2017
not expired

savedDate       Fri Jun 02 15:03:24 BST 2017
expiresDate     Sat Jun 03 15:03:24 BST 2017
expired

所有 日期/时间均为 UTC。首先没有过期。第二个已过期(savedDate 在 expiresDate 之前)。