找出一系列日期是否涵盖一个时间间隔

Find out if a series of dates covering an interval

我有两个对象日历

Calendar startCalendar = new GregorianCalendar(2013,0,31);

Calendar endCalendar = new GregorianCalendar();

我想知道上面列出的两个日期之间的间隔是否被n个其他对象对日历覆盖而没有间隔之间的空洞

示例 1:

Calendar startCalendar1(2013,0,31);
Calendar endCalendar1(2014,0,31);
Calendar startCalendar2(2013,5,31);
Calendar endCalendar2();

很好

示例 2:

Calendar startCalendar1(2013,0,31);
Calendar endCalendar1(2014,0,31);
Calendar startCalendar2(2014,2,31);
Calendar endCalendar2();

不好

我用Java6 谢谢

1 粗鲁但简单的方法

使用集合<长>

Set<Long> all_times_in_milli=new HashSet<Long>();
// Put every interval

// interval 1
for (long time_in_millis=startCalendar1.getTimeInMillis(); 
        time_in_millis<= endCalendar1.getTimeInMillis(); 
        time_in_millis+=86400000)
        all_times_in_milli.add(time_in_millis);

// interval 2
for (long time_in_millis=startCalendar2.getTimeInMillis(); 
        time_in_millis<= endCalendar2.getTimeInMillis(); 
        time_in_millis+=86400000)
        all_times_in_milli.add(time_in_millis);

// ETC
// AND TEST !
boolean failed=false;
for (long time_in_millis=startCalendar.getTimeInMillis(); 
        time_in_millis<= endCalendar.getTimeInMillis(); 
        time_in_millis+=86400000)
        {

        if (all_times_in_milli.contains(time_in_millis))
            {
            failed=true; break;
            }
        }

if (failed) System.out.println("Your are done !");

2 更聪明的方法 因为每个区间都是[long - long]区间

  • assemble 你的间隔得到连续间隔(重叠间隔集)=> 然后你得到 B1-E1、B2-E2、B3-E3 不同的间隔
  • 检查你的第一个区间是否在其中:B1 <= start <= end <=E1, 或 B2 <= 开始 <= 结束 <=E2, ...

只有当你有很多数据时才有趣

您使用的旧日期时间 classes 已被 Java 8 及更高版本中的 java.time 框架取代。那些旧的 classes 已被证明是笨重、混乱和有缺陷的。

java.time

java.time classes are inspired by the highly successful Joda‑Time library, intended as its successor, similar in concept but re-architected. Defined by JSR 310. Extended by the ThreeTen‑Extra project. See the Tutorial.

新的 classes 包括 LocalDate 用于没有时间的仅日期值。为了您的目的,请使用它而不是 Calendar

请注意,月份数字通常从一开始,这与 Calendar 不同。

LocalDate start = LocalDate.of( 2013 , 1 , 31 );

请注意,为了确定日期,时区至关重要。世界各地的日期并不同时相同。例如,巴黎的新一天比蒙特利尔早。

ZoneId zoneId = ZoneId.of ( "America/Montreal" );
LocalDate today = LocalDate.now ( zoneId );

从那里您可以调用 isAfterisBeforeisEqual 的任意组合来执行您的逻辑。您的问题并不清楚该逻辑,因此我无法解决。

扩展 java.time 的 ThreeTen-Extra 项目包括 Interval class that would help you. Unfortunately that class works only with Instant objects (date-time in UTC), not LocalDate. Specifically the methods for comparing intervals would help, abuts, encloses, and overlaps

您将为 LocalDate 对象创建自己的 IntervalLD class。通常我不建议滚动你自己的日期时间处理 classes 因为日期时间工作非常棘手。但在这种情况下 LocalDate 逻辑 可能 简单。这是我的快速草稿 完全未经测试的 示例,可帮助您入门。

package com.example.javatimestuffmaven;

import java.time.LocalDate;

/**
 * Similar to the 'Interval'class in the ThreeTen-Extra project, but for LocalDate objects.
 *
 * @author Basil Bourque
 */
public class IntervalLD {

    private LocalDate start, end;

    // Constructor
    public IntervalLD ( LocalDate startArg , LocalDate endArg ) {
        this.start = startArg;
        this.end = endArg;
    }

    public Boolean isBefore ( IntervalLD interval ) {
        // True if this one's end is before that one's start.
        boolean before = this.getEnd ().isBefore ( interval.getStart () );
        return before;
    }

    public Boolean isAfter ( IntervalLD interval ) {
        // True if this one's start is after that one's end.
        boolean after = this.getStart ().isAfter ( interval.getStart () );
        return after;
    }

    public Boolean abuts ( IntervalLD interval ) {
        // True if the intervals are next to each other on the time line but do not share a date. (exclusive of each other, not half-open)
        // True if either one's end is a day ahead of the other's start or vice versa, either's start is day after the other's end.
        if ( this.isBefore ( interval ) ) {
            if ( this.getEnd ().plusDays ( 1 ).equals ( interval.getStart () ) ) {
                return Boolean.TRUE;
            } else {
                return Boolean.FALSE;
            }
        } else if ( this.isAfter ( interval ) ) {
            if ( this.getStart ().minusDays ( 1 ).equals ( interval.getEnd () ) ) {
                return Boolean.TRUE;
            } else {
                return Boolean.FALSE;
            }
        } else if ( this.isEqual ( interval ) ) {
            return Boolean.FALSE;
        }

        // Impossible. Should never reach this point.
        // TODO: Handle this error condition.
        return Boolean.FALSE;
    }

    public Boolean encloses ( IntervalLD interval ) {
        //This checks if the specified interval is fully enclosed by this interval.
        // The result is true if the start of the specified interval is contained in this interval, and
        // the end is contained or equal to the end of this interval.
        boolean thatOneStartsOnOrAfterThisOne =  ! interval.getStart ().isBefore ( this.getStart () );
        boolean thatOneEndsOnOrAfterThisOne =  ! interval.getEnd ().isAfter ( this.getEnd () );
        boolean doesEnclose = ( thatOneStartsOnOrAfterThisOne && thatOneEndsOnOrAfterThisOne );
        return doesEnclose;
    }

    public Boolean overlaps ( IntervalLD interval ) {
        // True if the two intervals share some part of the timeline.
        // True if this interval does NOT start after that one ends OR this interval does NOT end before that one starts.
        boolean startsTooLate = this.getStart ().isAfter ( interval.getEnd () );
        boolean endsTooEarly = this.getEnd ().isAfter ( interval.getEnd () );
        boolean doesOverlap = (  ! startsTooLate &&  ! endsTooEarly );
        return ( doesOverlap );
    }

    public Boolean isEqual ( IntervalLD interval ) {
        boolean sameStart = this.getStart ().isEqual ( interval.getStart () );
        boolean sameEnd = this.getEnd ().isEqual ( interval.getEnd () );
        return ( sameStart && sameEnd );
    }

    @Override
    public String toString () {
        String output = this.getStart () + "/" + this.getEnd ();
        return output;
    }

    // Getters. Read-only (immutable) so no Setters.
    /**
     * @return the start
     */
    public LocalDate getStart () {
        return this.start;
    }

    /**
     * @return the end
     */
    public LocalDate getEnd () {
        return this.end;
    }
}

第一种方法:仅使用Java 6

当我看到像 2015-01-31 这样的日期示例时,我强烈怀疑您说的是 截止日期间隔 否则选择月末可能会出现有点奇怪。这是一种广泛传播且合理的方法。不幸的是,选择像 java.util.Calendar 这样的数据类型来表示一个瞬间(也是一个日期-时区-组合)与闭区间不一致。这种类似即时的类型在半开间隔下效果更好。结果是:

如果你决定只使用 Java-6-types 那么你可以尝试将所有 Calendar-objects 转换为 Long-values 代表自 Unix 纪元以来经过的毫秒数,正如@guillaume所建议的girod-vitouchkina(以我的赞成票为例,如何在没有任何外部库的情况下执行此操作)。 但是你必须提前为每个Calendar对象(如果代表结束边界)添加额外的一天,以达到关闭日期间隔的效果。

当然,您仍然需要自己做一些自己开发的区间算术,如该答案中粗略显示的那样。如果你仔细研究其他提案和你自己的需求,你会发现最终的解决方案甚至需要的不仅仅是一个新的区间 class 或者区间的基本比较。您还需要一个更高的抽象层,即在几个间隔之间定义操作。自己做这一切可能会引起一些头痛。另一方面:如果您具有良好的编程技能,那么实施基于 Long 的区间算法可能会节省一些性能开销,这是典型的额外区间库。

第二种方法:使用专用区间库

我只知道四个承诺处理间隔的库。无法使用@Basil Bourque 提到的Threeten-Extra,因为它需要Java-8。它的间隔 class 的缺点是只能处理瞬间,不能处理日历日期。也几乎不支持处理间隔集合。 Joda-Time 也是如此(它至少在 Java-6 上工作并且还提供专用的日历日期类型,即 LocalDate 但没有日期间隔)。

一个有趣的选择是使用 Guava 及其 class RangeSet,特别是如果您决定继续使用 Calendar-objects 和 Longs .这个 class 对处理间隔之间的操作有一些支持 - 对我来说比使用 Joda-Time 的简单间隔 class 更具吸引力。

最后,您还可以选择使用我的库 Time4J,其中包含 range-package。我现在将展示一个完整的解决方案来解决您的问题:

// our test interval
PlainDate start = PlainDate.of(2013, Month.JANUARY, 31);
PlainDate end = SystemClock.inLocalView().today();
DateInterval test = DateInterval.between(start, end);
IntervalCollection<PlainDate> icTest = IntervalCollection.onDateAxis().plus(test);

// two intervals for your GOOD case
PlainDate s1 = PlainDate.of(2013, Month.JANUARY, 31);
PlainDate e1 = PlainDate.of(2014, Month.JANUARY, 31);
DateInterval i1 = DateInterval.between(s1, e1);

PlainDate s2 = PlainDate.of(2013, Month.MAY, 31);
PlainDate e2 = end; // today
DateInterval i2 = DateInterval.between(s2, e2);

IntervalCollection<PlainDate> goodCase = 
    IntervalCollection.onDateAxis().plus(i1).plus(i2);

boolean covered = icTest.minus(goodCase).isEmpty();
System.out.println("Good case: " + covered); // true

// two intervals for your BAD case
PlainDate s3 = PlainDate.of(2013, Month.JANUARY, 31);
PlainDate e3 = PlainDate.of(2014, Month.JANUARY, 31);
DateInterval i3 = DateInterval.between(s3, e3);

PlainDate s4 = PlainDate.of(2014, Month.MARCH, 31);
PlainDate e4 = end; // today
DateInterval i4 = DateInterval.between(s4, e4);

IntervalCollection<PlainDate> badCase = 
    IntervalCollection.onDateAxis().plus(i3).plus(i4);

covered = icTest.minus(badCase).isEmpty();
System.out.println("Bad case: " + covered); // false

代码的最大部分只是区间构造。真正的区间运算本身是由这个小得惊人的代码片段完成的:

boolean covered = 
  IntervalCollection.onDateAxis().plus(test).minus(
    IntervalCollection.onDateAxis().plus(i1).plus(i2)
  ).isEmpty();

说明:如果test减去i1和i2的余数为空,则测试区间被区间i1和i2覆盖

顺便提一下:Time4J中的日期区间默认是闭区间。但是,如果您确实需要,可以将这些间隔更改为半开间隔(只需在给定的日期间隔内调用 withOpenEnd())。

如果您打算稍后迁移到 Java-8,您只需将 Time4J 版本更新到版本行 4.x(版本 v3.x 适用于 Java-6) 并非常容易地转换为 Java-8 类型,如 java.time.LocalDate(例如:PlainDate.from(localDate)LocalDate ld = plainDate.toTemporalAccessor()),因此您可以继续使用 Time4J 以获得额外功能即使在未来也不在标准 Java 范围内。