diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/AvailabilityTimeLine.java b/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/AvailabilityTimeLine.java
new file mode 100644
index 000000000..1df96cdc5
--- /dev/null
+++ b/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/AvailabilityTimeLine.java
@@ -0,0 +1,393 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * 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 .
+ */
+package org.navalplanner.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.lang.Validate;
+import org.joda.time.LocalDate;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public class AvailabilityTimeLine {
+
+ private static abstract class DatePoint implements Comparable {
+
+ 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);
+ }
+ }
+
+ }
+
+ private static class FixedPoint extends DatePoint {
+ private final LocalDate date;
+
+ private FixedPoint(LocalDate date) {
+ Validate.notNull(date);
+ this.date = 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();
+ }
+ }
+
+ private 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();
+ }
+
+ }
+
+ private 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();
+ }
+ }
+
+ private static class Interval implements
+ Comparable {
+
+ static Interval create(LocalDate start, LocalDate end) {
+ return new Interval(new FixedPoint(start), new FixedPoint(
+ end));
+ }
+
+ 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;
+ }
+
+ @Override
+ public int compareTo(Interval other) {
+ return this.start.compareTo(other.start) * 2
+ - this.end.compareTo(other.end);
+ }
+
+ 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 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 (DatePoint) Collections.min(Arrays.asList(values));
+ }
+
+ private DatePoint max(DatePoint... values) {
+ return (DatePoint) Collections.max(Arrays.asList(values));
+ }
+
+ }
+
+ public static AvailabilityTimeLine allValid() {
+ return new AvailabilityTimeLine();
+ }
+
+ private List invalids = new ArrayList();
+
+ private AvailabilityTimeLine() {
+ }
+
+ public boolean isValid(LocalDate date) {
+ if (invalids.isEmpty()) {
+ return true;
+ }
+ Interval point = Interval.point(date);
+ int binarySearch = Collections.binarySearch(invalids, point);
+ if (binarySearch >= 0) {
+ Interval interval = invalids.get(binarySearch);
+ return !interval.includes(date);
+ } else {
+ int insertionPoint = insertionPoint(binarySearch);
+ if (insertionPoint == 0) {
+ return true;
+ }
+ Interval interval = invalids
+ .get(insertionPoint - 1);
+ return !interval.includes(date);
+ }
+ }
+
+ public void invalidAt(LocalDate date) {
+ Interval point = Interval.point(date);
+ insert(point);
+ }
+
+ private void insert(Interval toBeInserted) {
+ int binarySearch = Collections.binarySearch(invalids, toBeInserted);
+ if (invalids.isEmpty()) {
+ invalids.add(toBeInserted);
+ return;
+ }
+ toBeInserted = coalesceWithAdjacent(insertionPoint(binarySearch),
+ toBeInserted);
+ int insertionPoint = insertBeforeAllAdjacent(toBeInserted);
+ removeAdjacent(insertionPoint, toBeInserted);
+ }
+
+ private int insertBeforeAllAdjacent(Interval toBeInserted) {
+ int n = Collections.binarySearch(invalids, toBeInserted);
+ int insertionPoint = insertionPoint(n);
+ invalids.add(insertionPoint, toBeInserted);
+ return insertionPoint;
+ }
+
+ public Interval coalesceWithAdjacent(int insertionPoint,
+ Interval toBeInserted) {
+ Interval result = toBeInserted;
+ for (int i = insertionPoint; i >= 0
+ && (i == invalids.size() || at(i).overlaps(
+ toBeInserted)); i--) {
+ if (i < invalids.size()) {
+ result = result.coalesce(at(i));
+ }
+ }
+ for (int i = insertionPoint; i < invalids.size()
+ && at(i).overlaps(toBeInserted); i++) {
+ result = result.coalesce(at(i));
+ }
+ return result;
+ }
+
+ private void removeAdjacent(int insertionPoint, Interval inserted) {
+ ListIterator 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));
+ }
+}
diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/AvailabilityTimeLineTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/AvailabilityTimeLineTest.java
new file mode 100644
index 000000000..c1e604b08
--- /dev/null
+++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/AvailabilityTimeLineTest.java
@@ -0,0 +1,183 @@
+/*
+ * This file is part of ###PROJECT_NAME###
+ *
+ * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e
+ * Desenvolvemento Tecnolóxico de Galicia
+ *
+ * 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 .
+ */
+package org.navalplanner.business.test.calendars.entities;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.joda.time.LocalDate;
+import org.junit.Test;
+import org.navalplanner.business.calendars.entities.AvailabilityTimeLine;
+
+/**
+ * @author Óscar González Fernández
+ *
+ */
+public class AvailabilityTimeLineTest {
+
+ private LocalDate earlyExample = new LocalDate(1000, 10, 6);
+
+ private LocalDate contemporaryExample = new LocalDate(2010, 10, 6);
+
+ private LocalDate lateExample = new LocalDate(3000, 10, 6);
+
+ @Test
+ public void anAllValidTimeLineIsValidForAllDates() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ assertTrue(timeLine.isValid(earlyExample));
+ assertTrue(timeLine.isValid(contemporaryExample));
+ assertTrue(timeLine.isValid(lateExample));
+ }
+
+ @Test
+ public void canBeAddedInvalidDates() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+
+ timeLine.invalidAt(earlyExample);
+
+ assertFalse(timeLine.isValid(earlyExample));
+ assertTrue(timeLine.isValid(contemporaryExample));
+ assertTrue(timeLine.isValid(lateExample));
+ }
+
+ @Test
+ public void canBeAddedInvalidIntervals() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+
+ LocalDate intervalStart = contemporaryExample.minusDays(10);
+ LocalDate intervalEnd = contemporaryExample.plusDays(5);
+ timeLine.invalidAt(intervalStart, intervalEnd);
+
+ assertFalse("the start is inclusive", timeLine.isValid(intervalStart));
+ assertFalse(timeLine.isValid(contemporaryExample.minusDays(1)));
+ assertFalse(timeLine.isValid(contemporaryExample));
+ assertFalse(timeLine.isValid(contemporaryExample.plusDays(1)));
+ assertTrue("the end is exclusive", timeLine.isValid(intervalEnd));
+ }
+
+ @Test
+ public void addingAnIntervalThatIsCompletelyInvalidIsIgnored() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ LocalDate intervalStart = contemporaryExample.minusDays(10);
+ LocalDate intervalEnd = contemporaryExample.plusDays(5);
+
+ timeLine.invalidAt(intervalStart, intervalEnd);
+ timeLine.invalidAt(intervalStart.plusDays(2), intervalEnd.minusDays(2));
+
+ assertFalse(timeLine.isValid(intervalEnd.minusDays(1)));
+ }
+
+ @Test
+ public void addingAnIntervalThatItsNotCompletelyInvalid() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ LocalDate intervalStart = contemporaryExample.minusDays(10);
+ LocalDate intervalEnd = contemporaryExample.plusDays(5);
+
+ timeLine.invalidAt(intervalStart, intervalEnd);
+ timeLine.invalidAt(intervalStart.minusDays(3), intervalEnd.plusDays(4));
+
+ assertFalse(timeLine.isValid(intervalStart.minusDays(3)));
+ assertFalse(timeLine.isValid(intervalEnd));
+ assertFalse(timeLine.isValid(intervalEnd.plusDays(3)));
+ assertTrue(timeLine.isValid(intervalEnd.plusDays(4)));
+ }
+
+ @Test
+ public void addingAnIntervalThatJoinsTwoInvalidIntervals() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ LocalDate intervalStart = contemporaryExample.minusDays(10);
+ LocalDate intervalEnd = contemporaryExample.plusDays(5);
+
+ timeLine.invalidAt(intervalStart, intervalEnd);
+ timeLine.invalidAt(intervalStart.minusDays(20), intervalStart
+ .minusDays(10));
+ timeLine.invalidAt(intervalStart.minusDays(10), intervalStart);
+ LocalDate current = intervalStart.minusDays(20);
+ while (current.isBefore(intervalEnd)) {
+ assertFalse(timeLine.isValid(current));
+ current = current.plusDays(1);
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void endMustBeAfterStart() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ LocalDate intervalStart = contemporaryExample.minusDays(10);
+ LocalDate intervalEnd = contemporaryExample.plusDays(5);
+ timeLine.invalidAt(intervalEnd, intervalStart);
+ }
+
+ @Test
+ public void addingFromInterval() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ timeLine.invalidFrom(contemporaryExample);
+
+ assertFalse(timeLine.isValid(contemporaryExample));
+ assertFalse(timeLine.isValid(contemporaryExample.plusDays(10)));
+ assertFalse(timeLine.isValid(lateExample));
+ assertTrue(timeLine.isValid(contemporaryExample.minusDays(1)));
+ }
+
+ @Test
+ public void addingToInterval() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ timeLine.invalidUntil(contemporaryExample);
+
+ assertTrue(timeLine.isValid(contemporaryExample));
+ assertTrue(timeLine.isValid(contemporaryExample.plusDays(1)));
+ assertFalse(timeLine.isValid(contemporaryExample.minusDays(1)));
+ assertFalse(timeLine.isValid(earlyExample));
+ }
+
+ @Test
+ public void addingAndAlreadyIncludedIntervalToAFromIntervalDoesNothing() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ timeLine.invalidFrom(contemporaryExample);
+ timeLine.invalidAt(contemporaryExample.plusDays(30),
+ contemporaryExample.plusDays(100));
+
+ assertFalse(timeLine.isValid(contemporaryExample));
+ assertFalse(timeLine.isValid(contemporaryExample.plusDays(10)));
+ assertFalse(timeLine.isValid(lateExample));
+ assertTrue(timeLine.isValid(contemporaryExample.minusDays(1)));
+ }
+
+ @Test
+ public void addingSeveralTypesOfIntervals() {
+ AvailabilityTimeLine timeLine = AvailabilityTimeLine.allValid();
+ timeLine.invalidFrom(contemporaryExample);
+ timeLine.invalidUntil(contemporaryExample.minusDays(10));
+
+ assertFalse(timeLine.isValid(earlyExample));
+ assertTrue(timeLine.isValid(contemporaryExample.minusDays(10)));
+
+ assertFalse(timeLine.isValid(contemporaryExample));
+ assertFalse(timeLine.isValid(lateExample));
+
+ timeLine.invalidAt(contemporaryExample.minusDays(10),
+ contemporaryExample);
+
+ assertFalse(timeLine.isValid(contemporaryExample));
+ assertFalse(timeLine.isValid(contemporaryExample.plusDays(1)));
+ assertFalse(timeLine.isValid(contemporaryExample.minusDays(1)));
+ assertFalse(timeLine.isValid(earlyExample));
+ }
+
+}