JAVA& Joda Time API:比较间隔,检测重叠并生成新的间隔

时间:2014-10-22 20:44:04

标签: java range jodatime overlap intersect

我正在制作一个令我困惑的项目。

给定List<TimeInterval> list包含类TimeInterval的元素,如下所示:

public class TimeInterval {
    private static final Instant CONSTANT = new Instant(0);
    private final LocalDate validFrom;
    private final LocalDate validTo;


    public TimeInterval(LocalDate validFrom, LocalDate validTo) {
        this.validFrom = validFrom;
        this.validTo = validTo;
    }


    public boolean isValid() {
        try {
            return toInterval() != null;
        }
        catch (IllegalArgumentException e) {
            return false;
        }
    }


    public boolean overlapsWith(TimeInterval timeInterval) {
        return this.toInterval().overlaps(timeInterval.toInterval());
    }


    private Interval toInterval() throws IllegalArgumentException {
        return new Interval(validFrom.toDateTime(CONSTANT), validTo.toDateTime(CONSTANT));
    }

使用以下内容生成间隔:

TimeInterval tI = new TimeInterval(ld_dateValidFrom, ld_dateValidTo);

列表中的间隔可能重叠:

|--------------------|
         |-------------------|

这应该导致:

|-------||-----------||------|

NOT 会导致:

|--------|-----------|-------|

一般来说,数字:

I1: 2014-01-01 - 2014-01-30
I2: 2014-01-07 - 2014-01-15

这应该导致:

I1: 2014-01-01 - 2014-01-06
I2: 2014-01-07 - 2014-01-15
I3: 2014-01-16 - 2014-01-30

我正在使用 JODA Time API ,但由于我第一次使用,我实际上并不知道如何解决我的问题。我已经看过方法overlap() / overlapWith(),但我仍然没有得到它。

非常感谢您的帮助!

更新 我找到了类似于我的问题的>here<,但这对我现在没有帮助。


我一遍又一遍地尝试过,虽然它在我测试的第一个时间间隔内有效,但它实际上并没有按照我想要的方式工作。

以下是我给出的时间间隔:

2014-10-20 ---> 2014-10-26
2014-10-27 ---> 2014-11-02
2014-11-03 ---> 2014-11-09
2014-11-10 ---> 2014-11-16
2014-11-17 ---> 9999-12-31

这是我用来生成新间隔的函数:

private List<Interval> cleanIntervalList(List<Interval> sourceList) {
    TreeMap<DateTime, Integer> endPoints = new TreeMap<DateTime, Integer>();

    // Fill the treeMap from the TimeInterval list. For each start point,
    // increment the value in the map, and for each end point, decrement it.
    for (Interval interval : sourceList) {
        DateTime start = interval.getStart();
        if (endPoints.containsKey(start)) {
            endPoints.put(start, endPoints.get(start)+1);
        }
        else {
            endPoints.put(start, 1);
        }
        DateTime end = interval.getEnd();
        if (endPoints.containsKey(end)) {
            endPoints.put(end, endPoints.get(start)-1);
        }
        else {
            endPoints.put(end, 1);
        }
    }
    System.out.println(endPoints);

    int curr = 0;
    DateTime currStart = null;

    // Iterate over the (sorted) map. Note that the first iteration is used
    // merely to initialize curr and currStart to meaningful values, as no
    // interval precedes the first point.

    List<Interval> targetList = new LinkedList<Interval>();

    for (Entry<DateTime, Integer> e : endPoints.entrySet()) {
        if (curr > 0) {
            if (e.getKey().equals(endPoints.lastEntry().getKey())){
                targetList.add(new Interval(currStart, e.getKey()));
            }
            else {
                targetList.add(new Interval(currStart, e.getKey().minusDays(1)));
            }
        }
        curr += e.getValue();
        currStart = e.getKey();
    }
    System.out.println(targetList);
    return targetList;
}

这就是输出实际上的样子:

2014-10-20 ---> 2014-10-25
2014-10-26 ---> 2014-10-26
2014-10-27 ---> 2014-11-01
2014-11-02 ---> 2014-11-02
2014-11-03 ---> 2014-11-08
2014-11-09 ---> 2014-11-09
2014-11-10 ---> 2014-11-15
2014-11-16 ---> 2014-11-16
2014-11-17 ---> 9999-12-31

这就是输出应该的样子:

2014-10-20 ---> 2014-10-26
2014-10-27 ---> 2014-11-02
2014-11-03 ---> 2014-11-09
2014-11-10 ---> 2014-11-16
2014-11-17 ---> 9999-12-31

由于原始区间没有重叠,我不明白为什么会产生像

这样的东西
2014-10-26 ---> 2014-10-26
2014-11-02 ---> 2014-11-02
2014-11-09 ---> 2014-11-09
etc

我一直在努力解决这个问题,但我仍然没有到达那里:(非常感谢任何帮助!

3 个答案:

答案 0 :(得分:5)

半开

我建议你重新考虑你的目标条款。 Joda-Time明智地使用&#34;半开放&#34;定义时间跨度的方法。开头是包含,而结尾是独占。例如,一周开始于第一天的开始,并在下周的第一个时刻运行,但不包括。半开放被证明是处理时间跨度的非常有用和自然的方式,正如其他答案中所讨论的那样。

enter image description here

在您的示例中使用此半开放方法,您确实需要此结果:

|--------|-----------|-------|

I1: 2014-01-01 - 2014-01-07
I2: 2014-01-07 - 2014-01-16
I3: 2014-01-16 - 2014-01-30

搜索StackOverflow&#34;半开&#34;找到讨论和例子,例如我的this answer

Joda-Time Interval

Joda-Time有一个很好的Interval类来表示时间轴上由一对端点定义的时间跨度。 Interval课程提供overlapoverlaps(原文如此),abutsgap方法。特别要注意的是overlap方法,它在比较其他两个时生成一个新的Interval;这可能是您解决方案的关键。

但不幸的是,该课程仅适用于DateTime个对象,而不适用于LocalDate(仅限日期,没有时间或时区)。也许缺乏对LocalDate的支持是您或您的团队发明TimeInterval课程的原因。但我建议使用该自定义类,考虑将DateTime对象与Joda-Time的类一起使用。我不是百分之百肯定比滚动你自己的日期间隔班更好(我很想做到这一点),但我的直觉告诉了我。

要关注日期而不是日期+时间,请在DateTime个对象上调用withTimeAtStartOfDay方法将时间部分调整为当天的第一时刻。第一个时刻通常是00:00:00.000,但不一定是由于夏令时(DST)和可能的其他异常。要小心并与时区保持一致;也许整个使用UTC。

以下是使用问题中建议的值在Joda-Time 2.5中的一些示例代码。在这些特定行中,对withTimeAtStartOfDay的调用可能是不必要的,因为Joda-Time默认为没有提供日期的第一时刻。但我建议使用withTimeAtStartOfDay这些调用,因为它会使您的代码自我记录您的意图。它使您{J}代码的所有日常使用都保持一致。

DateTime

从那里,应用其他答案中建议的逻辑。

答案 1 :(得分:2)

以下是基于您已找到的答案的建议算法。首先,您需要对间隔的所有端点进行排序。

TreeMap<LocalDate,Integer> endPoints = new TreeMap<LocalDate,Integer>();

此地图的键 - 由于这是一个TreeMap而排序 - 将是间隔开始和结束时的LocalDate对象。它们被映射到一个数字,该数字表示此日期的终点数量,从该日期的起点数量中减去。

现在遍历您的TimeInterval列表。对于每一个,对于起点,检查它是否已经在地图中。如果是这样,请在Integer中添加一个。如果没有,请将其添加到值为1的地图中。

对于相同间隔的终点,如果它存在于地图中,则从整数中减去1。如果没有,请使用值-1创建它。

填完endPoints后,为您要创建的“分解”区间创建一个新列表。

List<TimeInterval> newList = new ArrayList<TimeInterval>();

现在开始迭代endPoints。如果原始列表中至少有一个间隔,则endPoints中至少有两个点。你拿第一个,并将密钥(LocalDate)保存在变量currStart中,并将其关联的Integer保存在另一个变量(curr或其他东西)中。

循环从第二个元素开始直到结束。在每次迭代时:

  • 如果curr > 0,请从TimeInterval开始创建一个新的currStart,并在当前关键日期结束。将其添加到newList
  • 将整数值添加到curr
  • 将密钥指定为下一个currStart

等到最后。

这里发生的是:订购日期确保您没有重叠。保证每个新间隔不与任何新间隔重叠,因为它们具有独占和排序的端点。这里的技巧是在时间线中找到任何间隔都没有覆盖的空间。那些空白空间的特点是你的curr为零,这意味着在当前时间点之前开始的所有间隔也已经结束。端点之间的所有其他“空格”至少包含一个时间间隔,因此newList中应该有相应的新间隔。

这是一个实现,但请注意我没有使用Joda Time(我目前没有安装它,并且这里没有特殊功能需要它)。我创建了自己的基础TimeInterval类:

public class TimeInterval {
    private final Date validFrom;
    private final Date validTo;

    public TimeInterval(Date validFrom, Date validTo) {
        this.validFrom = validFrom;
        this.validTo = validTo;
    }

    public Date getStart() {
        return validFrom;
    }

    public Date getEnd() {
        return validTo;
    }

    @Override
    public String toString() {
        return "[" + validFrom + " - " + validTo + "]";
    }
}

重要的是为开始和结束添加访问器方法,以便能够在我编写时执行算法。实际上,如果你想使用他们的扩展功能,你应该使用Joda的Interval或实现他们的ReadableInterval

现在为方法本身。为了与您合作,您必须将所有Date更改为LocalDate

public static List<TimeInterval> breakOverlappingIntervals( List<TimeInterval> sourceList ) {

    TreeMap<Date,Integer> endPoints = new TreeMap<>();

    // Fill the treeMap from the TimeInterval list. For each start point, increment
    // the value in the map, and for each end point, decrement it.

    for ( TimeInterval interval : sourceList ) {
        Date start = interval.getStart();
        if ( endPoints.containsKey(start)) {
            endPoints.put(start, endPoints.get(start) + 1);
        } else {
            endPoints.put(start, 1);
        }
        Date end = interval.getEnd();
        if ( endPoints.containsKey(end)) {
            endPoints.put(end, endPoints.get(start) - 1);
        } else {
            endPoints.put(end, -1);
        }
    }

    int curr = 0;
    Date currStart = null;

    // Iterate over the (sorted) map. Note that the first iteration is used
    // merely to initialize curr and currStart to meaningful values, as no 
    // interval precedes the first point.

    List<TimeInterval> targetList = new ArrayList<>();
    for ( Map.Entry<Date,Integer> e : endPoints.entrySet() ) {
        if ( curr > 0 ) {
            targetList.add(new TimeInterval(currStart, e.getKey()));
        }
        curr += e.getValue();
        currStart = e.getKey();
    }
    return targetList;
}

(请注意,在这里使用类似Integer的可变对象而不是Integer可能更有效,但我选择了清晰度。)

答案 2 :(得分:0)

我没有完全掌握Joda的速度;如果你想要一个特定于重叠的解决方案,我需要阅读它。

但是,仅使用日期即可。这主要是伪代码,但应该说明问题。我还添加了符号,以便您可以分辨出间隔的样子。对于我是否应该为重叠添加1或减1来我也有一些困惑,所以我通过从重叠向外指向而小心谨慎(-1表示开始,+1表示结束)

TimeInterval a, b; //a and b are our two starting intervals
TimeInterval c = null;; //in case we have a third interval

if(a.start > b.start) { //move the earliest interval to a, latest to b, if necessary
    c = a;
    a = b;
    b = c;
    c = null;
}

if(b.start > a.start && b.start < a.end) { //case where b starts in the a interval
    if(b.end > a.end) { //b ends after a |AA||AB||BB|
        c = new TimeInterval(a.end + 1, b.end);//we need time interval c
        b.end = a.end;
        a.end = b.start - 1;
    }
    else if (b.end < a.end) { //b ends before a |AA||AB||AA|
        c = new TimeInterval(b.end + 1, a.end);//we need time interval c
        a.end = b.start - 1;
    }
    else { //b and a end at the same time, we don't need c |AA||AB|
        c = null;
        a.end = b.start - 1;
    }
}
else if(a.start == b.start) { //case where b starts same time as a
    if(b.end > a.end) { //b ends after a |AB||B|
        b.start = a.end + 1;
        a.end = a.end;
    }
    else if(b.end < a.end) { //b ends before a |AB||A|
        b.start = b.end + 1;
        b.end = a.end;
        a.end = b.start;
    }
    else { //b and a are the same |AB|
        b = null;
    }
}
else {
    //no overlap
}
相关问题