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

Posted

技术标签:

【中文标题】使用 LocalDate 类计算指定天数的日期差异【英文标题】:Calcuting the date difference for a specified number of days using LocalDate class 【发布时间】:2017-10-27 16:33:25 【问题描述】:

我正在使用 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;
    

我正在使用这样的类:

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 天内到期,并且用户要前往不同的时区。即他们可能基于小时领先或落后。许可证仍应过期。

【问题讨论】:

如果你使用的是java 8,你不需要使用org.threeten.bp,你可以使用java.time.LocalDate 正如 Hugo 所说,org.threeten.bp 来自 ThreeTen-Backport 项目。该库将大部分 java.time 功能向后移植到 Java 6 和 Java 7。Java 8 和 Java 9 中不需要向后移植,因为 java.time 是内置的。 请问,到期的具体时间是什么时候?这仅仅是保存时间后的 72 小时(3 * 24 = 72)吗?或者直到午夜?假设用户在 5 月 26 日上午 11 点 30 分点击了保存,那么是否应该在保存数据的时区的 5 月 29 日至 5 月 30 日之间的午夜到期?或者,第三个选项,在 3 天后的同一时钟时间?最后一个可能偶尔会给你 71 或 73 小时的时间来过渡到夏季时间(夏令时)。 @OleV.V.阅读您的评论应该是用户保存日期后的 72 小时。因此,当我们倒计时时,应该忽略午夜。 不要自己写时区代码,使用库youtube.com/watch?v=-5wpm-gesOY 【参考方案1】:

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

正如 Hugo 已经指出的那样,我会使用 java.time.LocalDate 并放弃使用 ThreeTen Backport(除非您的代码也可以在 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"));

如果您想从程序运行的 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 对象位于两个不同的时区,这也会执行正确的比较。因此,无论用户是否前往不同的时区,他/她都不会在许可证到期前获得更少或更多的小时数。

【讨论】:

是的,这是迄今为止发布的一个重要答案。 时区对于确定当前日期至关重要。因此,应明确指定预期/期望的区域。隐式依赖 JVM 的当前默认时区会导致不可靠的行为,因为该默认值可能是意料之外的,甚至可能被该 JVM 中任何应用程序中的任何代码更改运行时(!)。跨度> 如果用户要前往不同的时区,而不仅仅是位于亚洲,这会起作用吗? @ant2009 点是您很可能希望currentDatesavedDate 在同一时区中计算,或者使用包含时区的日期。这甚至应该适用于您在不同客户端上移动和测试日期的场景。【参考方案2】:

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

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

编辑(赏金解释后)

对于这个测试,我使用的是 threetenbp 1.3.4 版

由于您想要一个即使用户在不同时区也能工作的解决方案,所以您不应该使用LocalDate,因为此类不处理时区问题。

我认为最好的解决方案是使用Instant 类。它代表一个时间点,无论您在哪个时区(此时,世界上的每个人都在同一个即时,尽管本地日期和时间可能会因您所在的位置而异)。

实际上Instant 始终位于UTC Time - 一个标准的独立 时区,非常适合您的情况(因为您希望计算独立于用户所在的时区)。

所以你的savedDatecurrentDate 都必须是Instant 的,你应该计算它们之间的差异。

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

3 天 = 72 小时 72 小时后不到一秒,过期

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

    currentDatesavedDate 之后不到 72 小时 - 未过期 currentDate 正好在 savedDate 之后 72 小时 - 未过期(或已过期?请参阅下面的 cmets) currentDatesavedDate 晚了 72 小时以上(即使只有几分之一秒) - 过期

Instant 类具有纳秒精度,所以如果 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 小时 - 未过期 - 如果您希望它过期,只需将上面的if 更改为使用>= 而不是>

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


如果您不想要纳秒级精度而只想比较日期之间的天数,您可以像@Ole V.V's answer 中那样做。我相信我们的答案非常相似(我怀疑代码是等效的,尽管我不确定),但我没有测试足够的案例来检查它们在任何特定情况下是否不同。

【讨论】:

我将不得不使用 org.threeten.bp.LocalDate,因为我们必须支持以前的版本。 没问题,代码基本一样。 Threeten 是一个 backport,大部分功能都是由它实现的。 @ant2009 看到赏金解释后(它必须在不同的时区工作),我已经更新了答案【参考方案3】:

Hugo 的回答和 Ole V.V. 的回答都正确,the one by Ole V.V. is most important,因为时区对于确定当前日期至关重要。

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

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

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

Duration

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

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


关于java.time

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

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

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

从哪里获得 java.time 类?

Java SE 8Java SE 9 及更高版本 内置。 标准 Java API 的一部分,带有捆绑实现。 Java 9 添加了一些小功能和修复。 Java SE 6Java SE 7 ThreeTen-Backport 中的大部分 java.time 功能都向后移植到 Java 6 和 7。 Android ThreeTenABP 项目专门为 android 适配 ThreeTen-Backport(如上所述)。 见How to use ThreeTenABP…

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

【讨论】:

我将不得不使用 org.threeten.bp.LocalDate,因为我们必须支持以前的版本。 @ant2009 那么您不应该在问题的第一行中说您使用的是 Java 8。【参考方案4】:

我觉得用这个更好:

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

Duration 类放在java.time 包中。

【讨论】:

(A) 您希望在此处使用Period,而不是Duration,来计算基于日期的天数。 (B) 为什么“好多了”?在我看来,这两种方法似乎都同样适合。 @BasilBourque,(A)我想要Duration,如上所述。 (B) 好多了,因为我可以解决这个任务,如果我需要在另一个时间单位获得持续时间,这个解决方案效果很好。这是我基于个人经验的主观意见。 @BasilBourque,你知道 Period 是如何计算天数的吗?如果描述的情况如何使用? Period 是为LocalDate 明确构建的。如果您的“主观意见”未能解决问题中所述的具体需求,则它是无关紧要的。 @BasilBourque,我的解决方案在问题中失败了什么?【参考方案5】:

由于这个问题没有得到“足够的回应”,我添加了另一个答案:

我使用过“SimpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));”将时区设置为 UTC。所以不再有时区(所有日期/时间都将设置为 UTC)。

savedDate 设置为 UTC。

dateTimeNow 也设置为 UTC,过期的“天数”(负数)添加到 dateTimeNow

新日期 expiresDate 使用 dateTimeNow

中的长毫秒

检查是否保存日期。之前(过期日期)


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 之前)。

【讨论】:

【参考方案6】:

亲吻

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 是一天中的毫秒数。

如果您只使用 long 作为 savedTime,您可以进一步简化此操作,而不是使用 java.util.Date。

【讨论】:

这种方法不处理时区。 @chocksaway Doh。这就是她所说的,这就是 OP 想要的,在 Q 上阅读他的 cmets。【参考方案7】:

我构建了一个简单的实用程序类ExpiredDate,其中包含 TimeZone(例如 CET)、expiredDate、expireDays 和 differenceInHoursMillis。

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

查看 Date() multiplied by expiryDays plus(时区差multiplied by expiryDays)是否早于expiredDate.

任何早于 expiredDate 的日期都是“过期”。

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

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

+

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

新的日期parseDate() 返回。

如果 new Date 早于 expiredDate -> 设置 expired 为 True。 dateTimeWithExpire.before(expiredDate);

我创建了 3 个测试:

    设置有效期7天,expireDays = 3

    未过期(7天大于3天)

    设置过期日期/时间,并将 expireDays 设置为 2 天

    未过期 - 因为 CET 时区在 dateTimeWithExpire 的基础上增加了两个小时(每天一小时)

    设置有效期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");

        

      
 

【讨论】:

以上是关于使用 LocalDate 类计算指定天数的日期差异的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript计算指定日期与当前日期的相差天数

两个日期之间的天数差异[重复]

如何计算两个日期之间的差异?

hutool日期工具类相关:获取某月所有周某周的起止时间或所有日期计算连续天数

mysql: 如何计算指定日期到当前日期之间的天数

计算两个时间类型LocalDate合着LocalDateTime之间相差天数