JAVA и Joda Time API: сравнивайте интервалы, обнаруживайте перекрытия и генерируйте новые интервалы.

Я работаю над проектом, который меня очень сильно смущает прямо сейчас.

Дан 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);

Интервалы в списке могут перекрываться:

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

Это должно привести к:

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

Это должно НЕ привести к:

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

В общем, в цифрах:

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(), но до сих пор не понимаю.

Ваша помощь очень ценится!

ОБНОВЛЕНИЕ Я нашел что-то похожее на мою проблему >здесь‹ но это мне пока не помогает.


Я пробовал его снова и снова, и хотя он работал на первых интервалах, которые я тестировал, на самом деле он не работал так, как я хотел.

Вот интервалы, которые мне дали:

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

Я пытался исправить это весь день, и я все еще не дошел :( Любая дополнительная помощь очень ценится!


person Mosh Pit    schedule 22.10.2014    source источник
comment
Это может помочь вам с некоторыми из них.   -  person MadProgrammer    schedule 23.10.2014


Ответы (3)


Вот предлагаемый алгоритм, основанный на ответе, который вы уже нашли. Во-первых, вам нужно отсортировать все конечные точки интервалов.

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

Ключи этой карты, которые отсортированы, поскольку это TreeMap, будут объектами LocalDate в начале и в конце ваших интервалов. Они сопоставляются с числом, представляющим количество конечных точек на эту дату, вычтенное из количества начальных точек на эту дату.

Теперь просмотрите свой список TimeIntervals. Для каждого из них, для начальной точки, проверьте, есть ли она уже на карте. Если это так, добавьте один к Integer. Если нет, добавьте его на карту со значением 1.

Для конечной точки того же интервала, если она существует на карте, вычтите 1 из целого числа. Если нет, создайте его со значением -1.

Как только вы закончите заполнять endPoints, создайте новый список для «разбитых» интервалов, которые вы создадите.

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

Теперь начните перебирать endPoints. Если у вас был хотя бы один интервал в исходном списке, у вас будет как минимум две точки в endPoints. Вы берете первый и сохраняете ключ (LocalDate) в переменной currStart, а связанное с ним целое число — в другой переменной (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 + "]";
    }
}

Важно добавить методы доступа для начала и конца, чтобы иметь возможность выполнять алгоритм так, как я его написал. На самом деле вам, вероятно, следует использовать Interval Joda или реализовать их 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 здесь, но я выбрал для ясности).

person RealSkeptic    schedule 22.10.2014
comment
Спасибо за вашу помощь! Я посмотрю на это сегодня, но кажется, что это не совсем то, что я ищу, а именно (просто напомнить вам): I1: 2014-01-01 -- 05.01.2014, I2: 03.01.2014 -- 10.01.2014 ››› РЕЗУЛЬТАТЫ: 01.01.2014 -- 01.01.2014 02/2014-01-03 -- 2014-01-04 / 2014-01-05 -- 2014-01-10 - person Mosh Pit; 23.10.2014
comment
Это было бы потому, что ваши интервалы являются инклюзивными, или, по крайней мере, так вы их определяете. Вы можете настроить алгоритм для этого, добавляя день к конечным датам, когда вы добавляете их на карту, и вычитая, когда вы снова создаете новые интервалы. Обратите внимание, что интервалы Joda являются полуоткрытыми, и с ними намного проще работать, поэтому вам следует подумать о том, чтобы ваши интервалы были полуоткрытыми для манипуляций и переводили их обратно в закрытый конец только для отображения. - person RealSkeptic; 23.10.2014
comment
Что вы подразумеваете под изменяемым целочисленным объектом? - person Kamil; 12.04.2019

Полуоткрытый

Я предлагаю вам пересмотреть условия вашей цели. Joda-Time мудро использует "полуоткрытый" подход к определению промежутка времени. Начало включающее, а окончание исключающее. Например, неделя начинается с начала первого дня и продолжается до но не включая первого момента следующей недели. Полуоткрытость оказывается весьма полезным и естественным способом обработки промежутков времени, как обсуждалось в других ответах.

введите здесь описание изображения

Используя этот полуоткрытый подход для вашего примера, вы действительно хотите получить такой результат:

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

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

Найдите в StackOverflow слово «полуоткрытый», чтобы найти обсуждение и примеры, такие как этот мой ответ.

Joda-временной интервал

В Joda-Time есть отличный класс Interval. для представления промежутка времени, определяемого парой конечных точек на временной шкале. Этот класс Interval предлагает методы overlap, overlaps (sic), abuts и gap. Обратите особое внимание на overlap, генерирующий новый Interval при сравнении двух других; это может быть ключом к вашему решению.

Но, к сожалению, этот класс работает только с DateTime объектов, а не LocalDate (дата- только, без времени суток или часового пояса). Возможно, именно из-за отсутствия поддержки LocalDate вы или ваша команда изобрели этот класс TimeInterval. Но я предлагаю скорее использовать этот пользовательский класс, рассмотрите возможность использования объектов DateTime с классами Joda-Time. Я не уверен на 100%, что это лучше, чем создание собственного интервального класса только для даты (у меня был соблазн сделать это), но моя интуиция подсказывает мне это.

Чтобы сосредоточиться на днях, а не на день+время, для ваших DateTime объектов вызовите withTimeAtStartOfDay для настройки части времени на первый момент дня. Этот первый момент обычно 00:00:00.000, но не обязательно из-за перехода на летнее время (DST) и, возможно, других аномалий. Просто будьте осторожны и соблюдайте часовой пояс; возможно, используйте UTC повсюду.

Вот пример кода в Joda-Time 2.5 с использованием значений, предложенных в Вопросе. В этих конкретных строках вызов withTimeAtStartOfDay может быть ненужным, поскольку Joda-Time по умолчанию использует первый момент дня, когда день времени не указан. Но я предлагаю использовать эти вызовы withTimeAtStartOfDay, так как это делает ваш код самодокументируемым в соответствии с вашими намерениями. И это делает все ваше ежедневное использование кода DateTime согласованным.

Interval i1 = new Interval( new DateTime( "2014-01-01", DateTimeZone.UTC ).withTimeAtStartOfDay(), new DateTime( "2014-01-30", DateTimeZone.UTC ).withTimeAtStartOfDay() );
Interval i2 = new Interval( new DateTime( "2014-01-07", DateTimeZone.UTC ).withTimeAtStartOfDay(), new DateTime( "2014-01-15", DateTimeZone.UTC ).withTimeAtStartOfDay() );

Оттуда примените логику, предложенную в других ответах.

person Basil Bourque    schedule 23.10.2014

Я не совсем в курсе Джоды; Мне нужно будет прочитать об этом, если вам нужно решение для перекрытия.

Однако это возможно, используя только даты. В основном это псевдокод, но он должен донести суть. Я также добавил обозначения, чтобы вы могли сказать, как выглядят интервалы. У меня также есть некоторая путаница относительно того, следует ли мне прибавлять 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
}
person Compass    schedule 22.10.2014