【问题标题】:Find out if a series of dates covering an interval找出一系列日期是否覆盖一个区间
【发布时间】:2015-12-16 12:30:40
【问题描述】:

我有两个对象日历

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

不好

我使用 Java 6 谢谢

【问题讨论】:

  • 您在这里遇到什么问题?
  • 可能类似于thisthis
  • 你可以使用 Java 8 或 joda time 吗?
  • 我现在使用 Java 6 我不能使用 joda time
  • 不会将它们转换为原始长值并进行比较吗?

标签: java algorithm date calendar intervals


【解决方案1】:

第一种方法:仅使用 Java 6

当我看到像2015-01-31 这样的日期示例时,我强烈怀疑您所说的是关闭的日期间隔,否则选择月底可能会显得有点奇怪。这是一种广泛而合理的做法。不幸的是,选择像java.util.Calendar 这样的数据类型表示一个瞬间(也是一个日期-时区组合)与封闭间隔不一致。这种类似即时的类型在半开区间上效果更好。结果是:

如果您决定只使用 Java-6 类型,那么您可以尝试将所有 Calendar-objects 转换为表示自 Unix 纪元以来经过的毫秒数的长值,如 @guillaume girod-vitouchkina 所建议的(得到了我的赞成作为一个例子,如何在没有任何外部库的情况下做到这一点)。 但是你必须提前给每一个Calendar-object(如果代表一个结束边界)添加一个额外的一天,以达到封闭日期间隔的效果。

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

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

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

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

最后,您还可以选择使用我的库 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 的余数为空,则 test 区间被区间 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 未涵盖的额外功能。

【讨论】:

    【解决方案2】:

    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] 区间

    • 组合您的区间以获得连续区间(重叠区间集)=> 然后您将获得 B1-E1、B2-E2、B3-E3 不同区间
    • 检查您的第一个区间是否在其中:B1

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

    【讨论】:

      【解决方案3】:

      您使用的旧日期时间类已被 Java 8 及更高版本中的 java.time 框架取代。事实证明,这些旧课程笨拙、令人困惑且存在缺陷。

      java.time

      新的java.time 类的灵感来自非常成功的Joda‑Time 库,旨在作为其继任者,在概念上相似但经过重新架构。由JSR 310 定义。由ThreeTen‑Extra 项目扩展。请参阅Tutorial

      新的类包括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 类。不幸的是,该类仅适用于Instant 对象(UTC 日期时间),不适用于LocalDate。具体来说,比较区间的方法会有所帮助,abutsenclosesoverlaps

      您可以为 LocalDate 对象创建自己的 IntervalLD 类。通常我不建议推出你自己的日期时间处理类,因为日期时间工作非常棘手。但在这种情况下,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;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-04-19
        • 2021-08-20
        • 1970-01-01
        • 2019-08-23
        • 1970-01-01
        • 1970-01-01
        • 2022-11-26
        • 2013-04-23
        相关资源
        最近更新 更多