提问人:ant2009 提问时间:5/27/2017 最后编辑:ant2009 更新时间:6/5/2017 访问量:2764
使用 LocalDate 类计算指定天数的日期差
Calcuting the date difference for a specified number of days using LocalDate class
问:
我正在使用 openjdk 版本 1.8.0_112-release 进行开发,但也需要支持以前的 JDK 版本(Java-8 之前) - 所以不能使用 .java.time
我正在编写一个 utitily 类来计算日期,以查看保存的日期是否早于当前日期,这意味着它已过期。
但是,我不确定我是否以正确的方式执行此操作。我正在使用类来计算天数。过期时间从用户单击保存的日期和时间开始计算。该日期将被保存,并将根据此保存的日期和时间以及当前日期和时间(即用户登录时)进行检查。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 本机类,则在 中)来计算 2 个对象之间的天数:ChronoUnit.DAYS
org.threeten.bp.temporal
java.time.temporal
LocalDate
if (savedDate != null && currentDate != null) {
if (ChronoUnit.DAYS.between(savedDate, currentDate) > days) {
hasExpired = true;
}
}
编辑(赏金解释后)
对于此测试,我使用的是 threetenbp 版本 1.3.4
由于您想要一个即使用户处于不同时区也能正常工作的解决方案,因此不应使用 ,因为此类不处理时区问题。LocalDate
我认为最好的解决方案是使用该类。无论您身处哪个时区,它都代表一个时间点(此时此刻,世界上的每个人都处于同一时刻,尽管当地日期和时间可能因您所在的位置而异)。Instant
实际上总是在UTC时间 - 一个独立于时区的标准,因此非常适合您的情况(因为您想要独立于用户所在时区的计算)。Instant
所以 ur 和 must 都是 的,你应该计算它们之间的差值。savedDate
currentDate
Instant
现在,一个微妙的细节。您希望在 3 天后过期。对于我所做的代码,我做了以下假设:
- 3 天 = 72 小时
- 72 小时后 1 分之一秒,它已过期
第二个假设对于我实现解决方案的方式很重要。我正在考虑以下情况:
currentDate
不到 72 小时后 - 未过期savedDate
currentDate
正好是 72 小时后 - 未过期(或已过期? 请参阅下面的评论)savedDate
currentDate
超过 72 小时后(即使是几分之一秒)- 已过期savedDate
该类具有纳秒精度,因此在案例 3 中,我认为即使它在 1 小时后为 72 纳秒,它也已过期:Instant
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;
}
请注意,我曾经得到一天中的纳秒数。最好依靠 API,而不是硬编码容易出错的大数字。ChronoUnit.DAYS.getDuration().toNanos()
我做了一些测试,使用同一时区和不同时区的日期。
我使用方法将日期转换为: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
正好是savedDate后72小时 - 未过期) - 如果您希望它过期,只需将上述内容更改为使用,而不是:if
>=
>
if (ChronoUnit.NANOS.between(savedDate, currentDate) >= days * ChronoUnit.DAYS.getDuration().toNanos()) {
... // it returns "true" for case 2
}
如果你不想要纳秒精度,只想比较日期之间的天数,你可以像@Ole V.V.的答案一样。我相信我们的答案非常相似(我怀疑代码是等价的,尽管我不确定),但我还没有测试足够的案例来检查它们在任何特定情况下是否不同。
评论
我认为使用这个要好得多:
Duration.between(currentDate.atStartOfDay(), savedDate.atStartOfDay()).toDays() > days;
Duration
类放在包中。java.time
评论
Period
Duration
Duration
Period
是显式为 构建的。如果你的“主观意见”未能解决问题中陈述的具体需求,则该意见是无关紧要的。LocalDate
你的代码基本上没问题。我会以基本相同的方式做,只是一两个细节不同。
正如 Hugo 已经指出的那样,我会使用并放弃使用 ThreeTen Backport(除非您的代码也可以在 Java 6 或 7 上运行)。java.time.LocalDate
时区
你应该决定在哪个时区计算你的日子。此外,如果您在代码中明确时区,我更希望。如果您的系统仅在您自己的时区使用,那么选择很简单,只需明确说明即可。例如:
final LocalDate currentDate = LocalDate.now(ZoneId.of("Asia/Hong_Kong"));
请填写相关区域ID。这也将确保程序正常工作,即使有一天它碰巧在时区设置不正确的计算机上运行。如果您的系统是全局的,则可能需要使用 UTC,例如:
final LocalDate currentDate = LocalDate.now(ZoneOffset.UTC);
在保存用户单击“保存”的日期时,您需要执行类似的操作,以便您的数据保持一致。
72小时
编辑:我从您的评论中了解到,您想从保存时间开始测量 3 天,即 72 小时,以确定许可证是否已过期。为此,a 没有为您提供足够的信息。它只是一个没有时钟时间的日期,例如公元 2017 年 5 月 26 日。还有其他一些选项:LocalDate
Instant
是一个时间点(甚至纳秒精度)。这是确保 72 小时后过期的简单解决方案,无论用户是否移动到另一个时区。ZonedDateTime
表示日期、时间和时区,例如 2017 年 5 月 29 日,公元 19:21:33.783,偏移量 GMT+08:00[Asia/Hong_Kong]。如果您想提醒用户保存的时间,您将允许您显示该信息以及计算保存日期的时区。ZonedDateTime
- 最后也可以工作,但它似乎并没有给你带来其他两个的太多优势,所以我不会在这个选项上喋喋不休。
OffsetDateTime
由于每个时刻在所有时区中都是相同的,因此在获取当前时刻时无需指定时区:
final Instant currentDate = Instant.now();
将 3 天添加到 an 有点不同,但其余的逻辑是相同的: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;
}
另一方面,的用法与代码中的完全相同: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
评论
currentDate
savedDate
雨果的回答和奥勒的回答 V.V.两者都是正确的,Ole V.V. 的那个是最重要的,因为时区对于确定当前日期至关重要。
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
类的相似之处在于它表示未附加到时间线的时间跨度。但表示整秒加上以纳秒为单位解析的小数秒。由此,您可以计算出许多通用的 24 小时天(不是基于日期的天)、小时、分钟、秒和小数秒。请记住,日子并不总是 24 小时长;在美国,由于夏令时,它们目前可能是 23、24 或 25 小时。Period
Duration
这个问题是关于基于日期的天数,而不是 24 小时的块状物。所以这个类在这里不合适。Duration
关于java.time
java.time 框架内置于 Java 8 及更高版本中。这些类取代了麻烦的旧旧日期时间类,如 java.util.Date
、Calendar
和 SimpleDateFormat
。
Joda-Time 项目现在处于维护模式,建议迁移到 java.time 类。
要了解更多信息,请参阅 Oracle 教程。并在 Stack Overflow 中搜索许多示例和解释。规范是 JSR 310。
从哪里获取 java.time 类?
- Java SE 8、Java SE 9 及更高版本
- 内置。
- 具有捆绑实现的标准 Java API 的一部分。
- Java 9 增加了一些小功能和修复。
- Java SE 6 和 Java SE 7
- 在ThreeTen-Backport中,java.time的大部分功能都向后移植到Java 6和Java 7中。
- 人造人
- ThreeTenABP项目专门针对Android改编了ThreeTen-Backport(如上所述)。
- 请参阅如何使用ThreeTenABP...。
ThreeTen-Extra 项目使用其他类扩展了 java.time。这个项目是未来可能添加java.time的试验场。您可以在这里找到一些有用的类,例如 Interval
、YearWeek
、YearQuarter
等。
评论
吻
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 是一天中的毫秒数。
与其使用 java.util.Date,不如使用 long,如果您只使用 long 表示 savedTime,则可以进一步简化此操作。
评论
我构建了一个简单的实用程序类 ExpiredDate,其中包含 TimeZone(例如 CET)、expiredDate、expireDays 和 differenceInHoursMillis。
我使用 java.util.Date 和 Date.before(expiredDate):
查看 Date() 乘以 expiryDays 加(时区差异乘以 expiryDays)是否在 expiredDate 之前。
任何早于 expiredDate 的日期都是“已过期”。
通过添加 (i) + (ii) 创建一个新日期:
我使用一天中的毫秒数 (DAY_IN_MS = 1000 * 60 * 60 * 24) 乘以 (number of) expireDays。
+
为了处理不同的时区,我找到了默认时区(对我来说是 BST)和传递到 ExpiredDate 的时区(例如 CET)之间的毫秒数。对于 CET,差值为 1 小时,即 3600000 毫秒。这乘以 (number of) expireDays。
新的 Date 从 parseDate() 返回。
如果新 Date 早于 expiredDate ->则将 expired 设置为 True。dateTimeWithExpire.before(expiredDate);
我创建了 3 个测试:
将到期日期设置为 7 天,expireDays = 3
未过期(7 天大于 3 天)
将到期日期/时间设置为 2 天
未过期 - 因为 CET 时区向 dateTimeWithExpire 增加了两个小时(每天一小时)
将到期日期设置为 1 天,expireDays = 2(1 天小于 2 天)
expired 为 true
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");
}
}
}
由于这个问题没有得到“足够的回应”,我添加了另一个答案:
我使用“SimpleDateFormat.setTimeZone(TimeZone.getTimeZone(”UTC“));”将时区设置为UTC。因此,不再有时区(所有日期/时间都将设置为UTC)。
savedDate 设置为 UTC。
dateTimeNow 也设置为 UTC,并将过期的“天数”(负数)添加到 dateTimeNow。
新的 Date expiresDate 使用 dateTimeNow 中的长毫秒数
检查是否保存日期。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 之前)。
评论
org.threeten.bp
java.time.LocalDate