如何检查日期是否在特定范围内?

123
我有一系列带有开始日期和结束日期的范围。我想检查一个日期是否在这个范围内。
Date.before() 和 Date.after() 的使用似乎有点棘手。我真正需要的是类似于以下伪代码的东西:
boolean isWithinRange(Date testDate) {
    return testDate >= startDate && testDate <= endDate;
}

不确定是否相关,但我从数据库中获取的日期有时间戳。


1
类似的问题有多个答案:如何在Java中确定一个日期是否在两个日期之间? - Basil Bourque
2
值得注意的是,像java.util.Datejava.util.Calendar这样的老旧日期时间类现在已经成为遗留系统,被Java.time类所取代。请参阅Oracle的教程 - Basil Bourque
17个回答

197
boolean isWithinRange(Date testDate) {
   return !(testDate.before(startDate) || testDate.after(endDate));
}

我觉得这并不尴尬。请注意,我写成了那样而不是写成

return testDate.after(startDate) && testDate.before(endDate);

即使 testDate 恰好等于结束日期之一,它也能正常工作。


5
使用这个代码,以下是无效的:日期为2014年1月1日,范围从2014年1月1日到2014年1月31日。如何使其有效? - Shahe
4
@Shahe,我首先要做的是检查你日期中的时间部分。很有可能你的“date”是早上的时间,而“startDate”则在它之后。 - Paul Tomblin
2
这个使用否定或的技巧七年后仍然很不错。:) - Tosz
当testDate是今天的当前日期时,它无法正常工作。如何在上述建议的解决方案中处理这种情况。 - Swift
@Swift Date 包含时间(小时,分钟,秒和毫秒)。因此,如果您想测试某些内容是否为“今天”,则需要创建一个日期对象,该对象在时间0:00:00.000处为“今天”,并且在时间0:00:00.000处为“明天”。我使用 Calendar 类来实现,但是如果您使用 JodaTime 或较新的基本 Java 类,则肯定有更好的方法。 - Paul Tomblin

44

简而言之

ZoneId z = ZoneId.of( "America/Montreal" );  // A date only has meaning within a specific time zone. At any given moment, the date varies around the globe by zone.
LocalDate ld = 
    givenJavaUtilDate.toInstant()  // Convert from legacy class `Date` to modern class `Instant` using new methods added to old classes.
                     .atZone( z )  // Adjust into the time zone in order to determine date.
                     .toLocalDate();  // Extract date-only value.

LocalDate today = LocalDate.now( z );  // Get today’s date for specific time zone.
LocalDate kwanzaaStart = today.withMonth( Month.DECEMBER ).withDayOfMonth( 26 );  // Kwanzaa starts on Boxing Day, day after Christmas.
LocalDate kwanzaaStop = kwanzaaStart.plusWeeks( 1 );  // Kwanzaa lasts one week.
Boolean isDateInKwanzaaThisYear = (
    ( ! today.isBefore( kwanzaaStart ) ) // Short way to say "is equal to or is after".
    &&
    today.isBefore( kwanzaaStop )  // Half-Open span of time, beginning inclusive, ending is *exclusive*.
)

半开区间

日期时间常使用“半开”方式来定义一段时间范围。起始时间是包含在范围内的,而结束时间则是不包含在范围内的。因此,以星期一开始的一周持续到下一个星期一之前,但不包括下一个星期一。

java.time

Java 8及其后续版本内置了java.time框架,它取代了旧的麻烦类,包括java.util.Date/.CalendarSimpleDateFormat。受成功的Joda-Time库启发,由JSR 310定义,由ThreeTen-Extra项目扩展。

Instant是一个在UTC时间轴上具有纳秒分辨率的时刻。

Instant

java.util.Date对象转换为Instant对象。

Instant start = myJUDateStart.toInstant();
Instant stop =

如果通过JDBC从数据库获取java.sql.Timestamp对象,可以类似地转换为java.time.Instantjava.sql.Timestamp已经处于UTC时区,所以不需要担心时区问题。
Instant start = mySqlTimestamp.toInstant() ;
Instant stop =

获取当前时刻以进行比较。
Instant now = Instant.now();

使用isBefore,isAfter和equals方法进行比较。
Boolean containsNow = ( ! now.isBefore( start ) ) && ( now.isBefore( stop ) ) ;

LocalDate

也许您只想处理日期,而不是时间。

LocalDate类表示仅包含日期的值,没有时间和时区。

LocalDate start = LocalDate.of( 2016 , 1 , 1 ) ;
LocalDate stop = LocalDate.of( 2016 , 1 , 23 ) ;

要获取当前日期,请指定时区。在任何时刻,今天的日期都因时区而异。例如,在巴黎,新的一天比蒙特利尔早到来。
LocalDate today = LocalDate.now( ZoneId.of( "America/Montreal" ) );

我们可以使用 isEqualisBeforeisAfter 方法进行比较。在日期时间处理中,通常使用半开区间方法,其中时间跨度的开始是包含的,而结束是不包含的。请注意保留 HTML 标签。
Boolean containsToday = ( ! today.isBefore( start ) ) && ( today.isBefore( stop ) ) ;

间隔

如果你选择将ThreeTen-Extra库添加到你的项目中,你可以使用Interval类来定义时间跨度。该类提供了测试时间间隔是否包含, 毗邻, 包括, 或者重叠其他日期时间/时间间隔的方法。

Interval类适用于Instant对象。Instant类表示时间线上的一个时刻,以UTC为基准,分辨率为纳秒(最多九位小数)。

我们可以通过指定时区将LocalDate调整为一天中的第一个时刻,从而获得ZonedDateTime。然后,我们可以通过提取Instant来返回到UTC。

ZoneId z = ZoneId.of( "America/Montreal" );
Interval interval = 
    Interval.of( 
        start.atStartOfDay( z ).toInstant() , 
        stop.atStartOfDay( z ).toInstant() );
Instant now = Instant.now();
Boolean containsNow = interval.contains( now );

关于java.time

java.time框架内置于Java 8及以上版本。这些类替代了老旧的遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

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

要了解更多,请参阅Oracle教程。在Stack Overflow上搜索许多示例和解释。规范是JSR 310

如何获取java.time类?

ThreeTen-Extra 项目扩展了 java.time 的额外类。该项目是可能未来添加到java.time的一个试验场。您可能会在这里找到一些有用的类,例如 Interval, YearWeek, YearQuarter更多


21

那是正确的方式。日历的工作方式也是一样的。根据你的示例,我能提供的最佳方法是:

boolean isWithinRange(Date testDate) {
    return testDate.getTime() >= startDate.getTime() &&
             testDate.getTime() <= endDate.getTime();
}

Date.getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 起经过的毫秒数,它是一个 long 类型,因此可以轻松进行比较。


9
考虑使用Joda Time。我喜欢这个库,希望它能取代现有的Java Date和Calendar类的混乱。这是正确处理日期的方法。
编辑:现在已经不是2009年了,Java 8已经发布很长时间了。使用Java 8内置的基于Joda Time的java.time类,正如Basil Bourque上面提到的那样。在这种情况下,您将需要Period类,这里是Oracle的教程,介绍如何使用它。

3
如果一个人只想进行简单的比较,正如提问者所示,那么您认为将Joda Time引入项目是否过于繁琐? - hhafez
3
我理解你的意思是“过度”(overkill)。对于大多数库的添加,我同意你的看法。然而,Joda Time非常轻巧,通过其表现不可变性,帮助你编写更正确的日期处理代码,因此在我看来引入它并不是坏事。 - Ben Hardy
31
不要忘记 StackOverflow 规则 #46:如果有人在 Java 中提到日期或时间,必须有人建议切换到 Joda Time。不相信?试着找一个没有提到的问题。 - Paul Tomblin
2
即使Sun/Oracle也放弃了早期Java版本捆绑的旧日期时间类,同意用java.time框架(受Joda-Time启发)替换旧类。那些旧类设计不良,已被证明是麻烦和令人困惑的。 - Basil Bourque
2
@PaulTomblin ;-)规则#46已更新。现在强制要求有人提及并推荐Java.time,现代Java日期和时间API。正如这个答案也已经更新了。 - Ole V.V.
1
我很惊讶这仍然能够吸引人们的注意力。 - Ben Hardy

9

自从 Java 8

注意:除了一个构造函数之外,Date 的所有构造函数均已弃用。 大多数 Date 的方法也已弃用。 参考:Date: Deprecated Methods。 所有静态方法都已弃用,除了 Date::from(Instant)

因此,自 Java 8 以来,应该使用 Instant 类型(不可变、线程安全、支持闰秒),而不是 Date。 参考:Instant

  static final LocalTime MARKETS_OPEN = LocalTime.of(07, 00);
  static final LocalTime MARKETS_CLOSE = LocalTime.of(20, 00);

    // Instant utcTime = testDate.toInstant();
    var bigAppleTime = ZonedDateTime.ofInstant(utcTime, ZoneId.of("America/New_York"));

包括在范围内:

    return !bigAppleTime.toLocalTime().isBefore(MARKETS_OPEN)
        && !bigAppleTime.toLocalTime().isAfter(MARKETS_CLOSE);

在范围内但不包括边界值:

    return bigAppleTime.toLocalTime().isAfter(MARKETS_OPEN)
        && bigAppleTime.toLocalTime().isBefore(MARKETS_CLOSE);

1
有点意识到闰秒,但并不完全。 - Ole V.V.

4

一种简单的方法是将日期转换为1970年1月1日之后的毫秒数(使用Date.getTime()),然后比较这些值。


2
    public boolean isBetween(LocalDate target, LocalDate min, LocalDate max) {
    if (target == null || min == null || max == null) {
        throw new IllegalArgumentException("paramettres can't be null");
    }

    return target.compareTo(min) >= 0 && target.compareTo(max) <= 0;
}

2

如果您想进行包容性比较,并与 LocalDate 进行比较,则可以使用此代码。

LocalDate from = LocalDate.of(2021,8,1);
LocalDate to = LocalDate.of(2021,8,1);
LocalDate current = LocalDate.of(2021,8,1);

boolean isDateInRage = ( ! (current.isBefore(from.minusDays(1)) && current.isBefore(to.plusDays(1))) );

有点复杂,也许正因为如此并不完全正确。我将“current”更改为“LocalDate.of(2021, 7, 31);”,仍然得到了“true”,这显然是不正确的。使用“LocalDate”和“isBefore()”是个好主意。 - Ole V.V.

2

针对我的情况 -> 我有一个范围的开始和结束日期,以及一个日期列表,该列表可以部分或完全在提供的范围内(重叠)。

解决方案已经进行了测试:

/**
 * Check has any of quote work days in provided range.
 *
 * @param startDate inclusively
 * @param endDate   inclusively
 *
 * @return true if any in provided range inclusively
 */
public boolean hasAnyWorkdaysInRange(LocalDate startDate, LocalDate endDate) {
    if (CollectionUtils.isEmpty(workdays)) {
        return false;
    }

    LocalDate firstWorkDay = getFirstWorkDay().getDate();
    LocalDate lastWorkDay = getLastWorkDay().getDate();

    return (firstWorkDay.isBefore(endDate) || firstWorkDay.equals(endDate))
            && (lastWorkDay.isAfter(startDate) || lastWorkDay.equals(startDate));
}

1

这对我来说更清晰了。

// declare calendar outside the scope of isWithinRange() so that we initialize it only once
private Calendar calendar = Calendar.getInstance();

public boolean isWithinRange(Date date, Date startDate, Date endDate) {

    calendar.setTime(startDate);
    int startDayOfYear = calendar.get(Calendar.DAY_OF_YEAR); // first day is 1, last day is 365
    int startYear = calendar.get(Calendar.YEAR);

    calendar.setTime(endDate);
    int endDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
    int endYear = calendar.get(Calendar.YEAR);

    calendar.setTime(date);
    int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
    int year = calendar.get(Calendar.YEAR);

    return (year > startYear && year < endYear) // year is within the range
            || (year == startYear && dayOfYear >= startDayOfYear) // year is same as start year, check day as well
            || (year == endYear && dayOfYear < endDayOfYear); // year is same as end year, check day as well

}

2
(A) Calendar 类不是线程安全的。因此,维护一个可能被多个线程访问的实例是有风险的。(B) 诸如 java.util.Datejava.util.Calendar 等麻烦的旧类现在已经成为遗留系统,被 java.time 类所取代。请参阅 Oracle 教程 - Basil Bourque
@BasilBourque,感谢您指出这些问题。看起来Android还不支持新的DateTime类。 :/ - Dhunju_likes_to_Learn
1
大部分的java.time功能在ThreeTen-Backport中被回溯到Java 6和7,并进一步适应了AndroidThreeTenABP中(请参见如何使用...)。而且,这个问题与Android无关。 - Basil Bourque

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接