/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.business.calendars.entities; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.ListIterator; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.joda.time.LocalDate; /** * @author Óscar González Fernández <ogonzalez@igalia.com> * */ public class AvailabilityTimeLine { public static abstract class DatePoint implements Comparable<DatePoint> { protected abstract int compareTo(FixedPoint fixedPoint); protected abstract int compareTo(EndOfTime endOfTime); protected abstract int compareTo(StartOfTime startOfTime); protected abstract boolean equalTo(FixedPoint fixedPoint); protected abstract boolean equalTo(EndOfTime endOfTime); protected abstract boolean equalTo(StartOfTime startOfTime); @Override public final int compareTo(DatePoint obj) { Validate.notNull(obj); if ( obj instanceof FixedPoint ) { return compareTo((FixedPoint) obj); } else if ( obj instanceof EndOfTime ) { return compareTo((EndOfTime) obj); } else if ( obj instanceof StartOfTime ) { return compareTo((StartOfTime) obj); } else { throw new RuntimeException("unknown subclass for " + obj); } } @Override public abstract int hashCode(); @Override public final boolean equals(Object obj) { if ( !(obj instanceof DatePoint) ) { return false; } if ( obj instanceof FixedPoint ) { return equalTo((FixedPoint) obj); } else if ( obj instanceof EndOfTime ) { return equalTo((EndOfTime) obj); } else if ( obj instanceof StartOfTime ) { return equalTo((StartOfTime) obj); } else { throw new RuntimeException("unknown subclass for " + obj); } } @Override public abstract String toString(); } public static class FixedPoint extends DatePoint { private final LocalDate date; public FixedPoint(LocalDate date) { Validate.notNull(date); this.date = date; } public LocalDate getDate() { return date; } @Override protected int compareTo(FixedPoint fixedPoint) { return this.date.compareTo(fixedPoint.date); } @Override protected int compareTo(EndOfTime endOfTime) { return -1; } @Override protected int compareTo(StartOfTime startOfTime) { return 1; } @Override protected boolean equalTo(FixedPoint fixedPoint) { return date.equals(fixedPoint.date); } @Override protected boolean equalTo(EndOfTime endOfTime) { return false; } @Override protected boolean equalTo(StartOfTime startOfTime) { return false; } @Override public int hashCode() { return date.hashCode(); } @Override public String toString() { return date.toString(); } public static LocalDate tryExtract(DatePoint start) { FixedPoint point = (FixedPoint) start; return point.getDate(); } } public static class EndOfTime extends DatePoint { private static final EndOfTime INSTANCE = new EndOfTime(); public static EndOfTime create() { return INSTANCE; } @Override protected int compareTo(FixedPoint fixedPoint) { return 1; } @Override protected int compareTo(EndOfTime endOfTime) { return 0; } @Override protected int compareTo(StartOfTime startOfTime) { return 1; } @Override protected boolean equalTo(FixedPoint fixedPoint) { return false; } @Override protected boolean equalTo(EndOfTime endOfTime) { return true; } @Override protected boolean equalTo(StartOfTime startOfTime) { return false; } @Override public int hashCode() { return EndOfTime.class.hashCode(); } @Override public String toString() { return EndOfTime.class.getSimpleName(); } } public static class StartOfTime extends DatePoint { private static final StartOfTime INSTANCE = new StartOfTime(); public static StartOfTime create() { return INSTANCE; } @Override protected int compareTo(FixedPoint fixedPoint) { return -1; } @Override protected int compareTo(EndOfTime endOfTime) { return -1; } @Override protected int compareTo(StartOfTime startOfTime) { return 0; } @Override protected boolean equalTo(FixedPoint fixedPoint) { return false; } @Override protected boolean equalTo(EndOfTime endOfTime) { return false; } @Override protected boolean equalTo(StartOfTime startOfTime) { return true; } @Override public int hashCode() { return StartOfTime.class.hashCode(); } @Override public String toString() { return StartOfTime.class.getSimpleName(); } } public static class Interval implements Comparable<Interval> { /** * Creates an interval. Null values can be provided. * * @param start * if <code>null</code> is interpreted as start of time. * @param end * if <code>null</code> is interpreted as end of time * @return an interval from start to end */ public static Interval create(LocalDate start, LocalDate end) { DatePoint startPoint = start == null ? new StartOfTime() : new FixedPoint(start); DatePoint endPoint = end == null ? new EndOfTime() : new FixedPoint(end); return new Interval(startPoint, endPoint); } static Interval all() { return new Interval(StartOfTime.create(), EndOfTime.create()); } static Interval from(LocalDate date) { return new Interval(new FixedPoint(date), EndOfTime.create()); } public static Interval to(LocalDate date) { return new Interval(StartOfTime.create(), new FixedPoint(date)); } static Interval point(LocalDate start) { return new Interval(new FixedPoint(start), new FixedPoint(start.plusDays(1))); } private final DatePoint start; private final DatePoint end; private Interval(DatePoint start, DatePoint end) { this.start = start; this.end = end; } public DatePoint getStart() { return start; } public DatePoint getEnd() { return end; } @Override public int compareTo(Interval other) { return this.start.compareTo(other.start) * 2 - this.end.compareTo(other.end); } @Override public boolean equals(Object obj) { if ( obj instanceof Interval ) { Interval other = (Interval) obj; return start.equals(other.getStart()) && end.equals(other.getEnd()); } return false; } @Override public int hashCode() { return new HashCodeBuilder().append(start).append(end).toHashCode(); } public boolean includes(LocalDate date) { return includes(new FixedPoint(date)); } private boolean includes(FixedPoint point) { return start.equals(point) || start.compareTo(point) <= 0 && point.compareTo(end) < 0; } public boolean overlaps(Interval other) { return start.compareTo(other.end) <= 0 && end.compareTo(other.start) >= 0; } public Interval intersect(Interval other) { Validate.isTrue(overlaps(other)); return new Interval(max(start, other.start), min(end, other.end)); } public Interval coalesce(Interval other) { if ( !overlaps(other) ) { throw new IllegalArgumentException("in order to coalesce two intervals must overlap"); } return new Interval(min(start, other.start), max(end, other.end)); } private DatePoint min(DatePoint... values) { return Collections.min(Arrays.asList(values)); } private DatePoint max(DatePoint... values) { return Collections.max(Arrays.asList(values)); } @Override public String toString() { return String.format("[%s, %s]", start, end); } } public interface IVetoer { boolean isValid(LocalDate date); } public static AvailabilityTimeLine allValid() { return new AvailabilityTimeLine(); } public static AvailabilityTimeLine createAllInvalid() { AvailabilityTimeLine result = new AvailabilityTimeLine(); result.allInvalid(); return result; } private static IVetoer NO_VETOER = new IVetoer() { @Override public boolean isValid(LocalDate date) { return true; } }; private IVetoer vetoer = NO_VETOER; private List<Interval> invalids = new ArrayList<>(); private AvailabilityTimeLine() { } public boolean isValid(LocalDate date) { return isValidBasedOnInvaidIntervals(date) && vetoer.isValid(date); } private boolean isValidBasedOnInvaidIntervals(LocalDate date) { if ( invalids.isEmpty() ) { return true; } Interval possibleInterval = findPossibleIntervalFor(date); return possibleInterval == null || !possibleInterval.includes(date); } private Interval findPossibleIntervalFor(LocalDate date) { Interval point = Interval.point(date); int binarySearch = Collections.binarySearch(invalids, point); if ( binarySearch >= 0) { return invalids.get(binarySearch); } else { int insertionPoint = insertionPoint(binarySearch); if ( insertionPoint == 0 ) { return null; } return invalids.get(insertionPoint - 1); } } public void allInvalid() { insert(Interval.all()); } public void invalidAt(LocalDate date) { Interval point = Interval.point(date); insert(point); } /** * There are some invalid dates that cannot or are not suitable to be * represented as belonging to invalid intervals. For example if the invalid * dates are an infinite set. * * @param vetoer * the vetoer to use */ public void setVetoer(IVetoer vetoer) { Validate.notNull(vetoer); this.vetoer = vetoer; } private void insert(Interval toBeInserted) { if ( invalids.isEmpty() ) { invalids.add(toBeInserted); return; } toBeInserted = coalesceWithAdjacent(toBeInserted); int insertionPoint = insertBeforeAllAdjacent(toBeInserted); removeAdjacent(insertionPoint, toBeInserted); } /** * Returns the insertion position for the interval. Inserting the interval * at that position guarantees that interval start is posterior or equal to * any previous interval start. If the next interval start is equal to the * interval, the length of the former is less than the latter */ private int findInsertionPosition(Interval interval) { int binarySearch = Collections.binarySearch(invalids, interval); return insertionPoint(binarySearch); } private int insertBeforeAllAdjacent(Interval toBeInserted) { int insertionPoint = findInsertionPosition(toBeInserted); invalids.add(insertionPoint, toBeInserted); return insertionPoint; } private Interval coalesceWithAdjacent(Interval toBeInserted) { Interval result = toBeInserted; List<Interval> adjacent = getAdjacent(toBeInserted); for (Interval each : adjacent) { result = result.coalesce(each); } return result; } private List<Interval> getAdjacent(Interval toBeInserted) { final int insertionPoint = findInsertionPosition(toBeInserted); List<Interval> result = new ArrayList<>(); assert insertionPoint <= invalids.size(); for (int i = insertionPoint - 1; i >= 0 && at(i).overlaps(toBeInserted); i--) { result.add(at(i)); } for (int i = insertionPoint; i < invalids.size() && at(i).overlaps(toBeInserted); i++) { result.add(at(i)); } return result; } private List<Interval> intersectWithAdjacent(Interval interval) { List<Interval> result = new ArrayList<>(); List<Interval> adjacent = getAdjacent(interval); for (Interval each : adjacent) { assert interval.overlaps(each); result.add(interval.intersect(each)); } return result; } private void removeAdjacent(int insertionPoint, Interval inserted) { ListIterator<Interval> listIterator = invalids.listIterator(insertionPoint + 1); while (listIterator.hasNext()) { Interval next = listIterator.next(); if ( !next.overlaps(inserted) ) { break; } listIterator.remove(); } } private Interval at(int i) { return i >= 0 && i < invalids.size() ? invalids.get(i) : null; } private int insertionPoint(int binarySearchResult) { return binarySearchResult < 0 ? (-binarySearchResult) - 1 : binarySearchResult; } public void invalidAt(LocalDate intervalStart, LocalDate intervalEnd) { if ( intervalStart.isAfter(intervalEnd) ) { throw new IllegalArgumentException("end must be equal or after start"); } insert(Interval.create(intervalStart, intervalEnd)); } public void invalidFrom(LocalDate date) { insert(Interval.from(date)); } public void invalidUntil(LocalDate date) { insert(Interval.to(date)); } public AvailabilityTimeLine and(AvailabilityTimeLine another) { AvailabilityTimeLine result = AvailabilityTimeLine.allValid(); inserting(result, invalids); inserting(result, another.invalids); result.setVetoer(and(this.vetoer, another.vetoer)); return result; } private static IVetoer and(final IVetoer a, final IVetoer b) { return new IVetoer() { @Override public boolean isValid(LocalDate date) { return a.isValid(date) && b.isValid(date); } }; } public AvailabilityTimeLine or(AvailabilityTimeLine another) { List<Interval> intersections = doIntersections(this, another); AvailabilityTimeLine result = AvailabilityTimeLine.allValid(); for (Interval each : intersections) { boolean fromStartOfTime = each.getStart().equals(StartOfTime.create()); boolean untilEndOfTime = each.getEnd().equals(EndOfTime.create()); if ( fromStartOfTime && untilEndOfTime ) { result.allInvalid(); } else if ( fromStartOfTime ) { result.invalidUntil(FixedPoint.tryExtract(each.getEnd())); } else if ( untilEndOfTime ) { result.invalidFrom(FixedPoint.tryExtract(each.getStart())); } else { result.invalidAt(FixedPoint.tryExtract(each.getStart()), FixedPoint.tryExtract(each.getEnd())); } } result.setVetoer(or(this.vetoer, another.vetoer)); return result; } private static IVetoer or(final IVetoer a, final IVetoer b) { return new IVetoer() { @Override public boolean isValid(LocalDate date) { return a.isValid(date) || b.isValid(date); } }; } private static List<Interval> doIntersections(AvailabilityTimeLine one, AvailabilityTimeLine another) { List<Interval> result = new ArrayList<>(); for (Interval each : one.invalids) { result.addAll(another.intersectWithAdjacent(each)); } return result; } private void inserting(AvailabilityTimeLine result, List<Interval> invalid) { for (Interval each : invalid) { result.insert(each); } } public List<Interval> getValidPeriods() { List<Interval> result = new ArrayList<>(); DatePoint previous = StartOfTime.create(); for (Interval each : invalids) { DatePoint invalidStart = each.start; if ( !invalidStart.equals(StartOfTime.create()) && !invalidStart.equals(EndOfTime.create()) ) { result.add(new Interval(previous, invalidStart)); } previous = each.getEnd(); } if ( !previous.equals(EndOfTime.create()) ) { result.add(new Interval(previous, EndOfTime.create())); } return result; } }