diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java index 228538b3e..f60a00324 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/ResourceAllocation.java @@ -144,6 +144,41 @@ public abstract class ResourceAllocation extends }; } + public enum Direction { + FORWARD { + @Override + public IntraDayDate getDateFromWhichToAllocate(Task task) { + return IntraDayDate.max(task.getFirstDayNotConsolidated(), + task.getIntraDayStartDate()); + } + + @Override + void limitAvailabilityOn(AvailabilityTimeLine availability, + IntraDayDate dateFromWhichToAllocate) { + availability.invalidUntil(dateFromWhichToAllocate + .asExclusiveEnd()); + } + }, + BACKWARD { + @Override + public IntraDayDate getDateFromWhichToAllocate(Task task) { + return task.getIntraDayEndDate(); + } + + @Override + void limitAvailabilityOn(AvailabilityTimeLine availability, + IntraDayDate dateFromWhichToAllocate) { + availability.invalidFrom(dateFromWhichToAllocate.getDate()); + } + }; + + public abstract IntraDayDate getDateFromWhichToAllocate(Task task); + + abstract void limitAvailabilityOn(AvailabilityTimeLine availability, + IntraDayDate dateFromWhichToAllocate); + + } + public static AllocationsSpecified allocating( List resourceAllocations) { return new AllocationsSpecified(resourceAllocations); @@ -229,7 +264,12 @@ public abstract class ResourceAllocation extends } public IntraDayDate untilAllocating(int hoursToAllocate) { - return untilAllocating(hoursToAllocate, doNothing()); + return untilAllocating(Direction.FORWARD, hoursToAllocate); + } + + public IntraDayDate untilAllocating(Direction direction, + int hoursToAllocate) { + return untilAllocating(direction, hoursToAllocate, doNothing()); } private static INotFulfilledReceiver doNothing() { @@ -244,7 +284,13 @@ public abstract class ResourceAllocation extends public IntraDayDate untilAllocating(int hoursToAllocate, final INotFulfilledReceiver receiver) { + return untilAllocating(Direction.FORWARD, hoursToAllocate, receiver); + } + + public IntraDayDate untilAllocating(Direction direction, + int hoursToAllocate, final INotFulfilledReceiver receiver) { UntilFillingHoursAllocator allocator = new UntilFillingHoursAllocator( + direction, task, allocations) { @Override @@ -256,17 +302,23 @@ public abstract class ResourceAllocation extends @Override protected void setNewDataForAllocation( - ResourceAllocation allocation, IntraDayDate end, + ResourceAllocation allocation, + IntraDayDate resultDate, ResourcesPerDay resourcesPerDay, List dayAssignments) { Task task = AllocationsSpecified.this.task; - allocation.resetAssignmentsTo(dayAssignments, - task.getIntraDayStartDate(), end); + if (isForwardScheduling()) { + allocation.resetAssignmentsTo(dayAssignments, + task.getIntraDayStartDate(), resultDate); + } else { + allocation.resetAssignmentsTo(dayAssignments, + resultDate, task.getIntraDayEndDate()); + } allocation.updateResourcesPerDay(); } @Override protected CapacityResult thereAreAvailableHoursFrom( - IntraDayDate start, + IntraDayDate dateFromWhichToAllocate, ResourcesPerDayModification resourcesPerDayModification, EffortDuration effortToAllocate) { ICalendar calendar = getCalendar(resourcesPerDayModification); @@ -274,7 +326,8 @@ public abstract class ResourceAllocation extends .getGoal(); AvailabilityTimeLine availability = resourcesPerDayModification .getAvailability(); - availability.invalidUntil(start.asExclusiveEnd()); + getDirection().limitAvailabilityOn(availability, + dateFromWhichToAllocate); return ThereAreHoursOnWorkHoursCalculator .thereIsAvailableCapacityFor(calendar, availability, resourcesPerDay, diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/ResourcesPerDayModification.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/ResourcesPerDayModification.java index e5c85c999..76309a9a5 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/ResourcesPerDayModification.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/ResourcesPerDayModification.java @@ -254,4 +254,9 @@ public abstract class ResourcesPerDayModification extends return dayDuration.compareTo(date.getEffortDuration()) > 0; } + public EffortDuration durationAtDay(PartialDay day) { + return getBeingModified().getAllocationCalendar().asDurationOn(day, + goal); + } + } diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/UntilFillingHoursAllocator.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/UntilFillingHoursAllocator.java index 43bf5db2c..54c845f46 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/UntilFillingHoursAllocator.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/UntilFillingHoursAllocator.java @@ -31,10 +31,12 @@ import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang.Validate; +import org.joda.time.LocalDate; import org.navalplanner.business.calendars.entities.ThereAreHoursOnWorkHoursCalculator.CapacityResult; import org.navalplanner.business.common.ProportionalDistributor; import org.navalplanner.business.planner.entities.DayAssignment; import org.navalplanner.business.planner.entities.ResourceAllocation; +import org.navalplanner.business.planner.entities.ResourceAllocation.Direction; import org.navalplanner.business.planner.entities.Task; import org.navalplanner.business.workingday.EffortDuration; import org.navalplanner.business.workingday.IntraDayDate; @@ -43,14 +45,18 @@ import org.navalplanner.business.workingday.ResourcesPerDay; public abstract class UntilFillingHoursAllocator { + private final Direction direction; + private final Task task; private List allocations; private Map> resultAssignments = new HashMap>(); - public UntilFillingHoursAllocator(Task task, + + public UntilFillingHoursAllocator(Direction direction, Task task, List allocations) { + this.direction = direction; this.task = task; this.allocations = allocations; initializeResultsMap(); @@ -63,88 +69,160 @@ public abstract class UntilFillingHoursAllocator { } public IntraDayDate untilAllocating(EffortDuration effortToAllocate) { - final IntraDayDate start = IntraDayDate.max( - task.getFirstDayNotConsolidated(), task.getIntraDayStartDate()); - List effortPerAllocation = effortPerAllocation(start, - effortToAllocate); + final IntraDayDate dateFromWhichToAllocate = direction + .getDateFromWhichToAllocate(task); + List effortPerAllocation = effortPerAllocation( + dateFromWhichToAllocate, effortToAllocate); if (effortPerAllocation.isEmpty()) { return null; } - return untilAllocating(start, effortPerAllocation); + return untilAllocating(dateFromWhichToAllocate, effortPerAllocation); } - private IntraDayDate untilAllocating(final IntraDayDate start, + private IntraDayDate untilAllocating(final IntraDayDate dateFromWhichToAllocate, List effortPerAllocation) { int i = 0; - IntraDayDate currentEnd = start; + IntraDayDate currentResult = dateFromWhichToAllocate; for (EffortPerAllocation each : effortPerAllocation) { - IntraDayDate endCandidate = untilAllocating(start, each.allocation, - each.duration); - currentEnd = IntraDayDate.max(currentEnd, endCandidate); + IntraDayDate candidate = untilAllocating(dateFromWhichToAllocate, + each.allocation, each.duration); + currentResult = pickCurrentOrCandidate(currentResult, candidate); i++; } - setAssignmentsForEachAllocation(currentEnd); - return currentEnd; + setAssignmentsForEachAllocation(currentResult); + return currentResult; } - private List effortPerAllocation(IntraDayDate start, - EffortDuration toBeAssigned) { + private IntraDayDate pickCurrentOrCandidate(IntraDayDate current, + IntraDayDate candidate) { + if (direction == Direction.BACKWARD) { + return IntraDayDate.min(current, candidate); + } + return IntraDayDate.max(current, candidate); + } + + private List effortPerAllocation( + IntraDayDate dateFromWhichToAllocate, EffortDuration toBeAssigned) { return new HoursPerAllocationCalculator(allocations) - .calculateEffortsPerAllocation(start, toBeAssigned); + .calculateEffortsPerAllocation(dateFromWhichToAllocate, + toBeAssigned); } /** * - * @param start + * @param dateFromWhichToAllocate * @param resourcesPerDayModification * @param effortRemaining * @return the moment on which the allocation would be completed */ - private IntraDayDate untilAllocating(IntraDayDate start, + private IntraDayDate untilAllocating(IntraDayDate dateFromWhichToAllocate, ResourcesPerDayModification resourcesPerDayModification, EffortDuration effortRemaining) { EffortDuration taken = zero(); - IntraDayDate lastDate = start; - for (IntraDayDate current = start; effortRemaining.compareTo(zero()) > 0; current = current.nextDayAtStart()) { - lastDate = current; - taken = assignForDay(resourcesPerDayModification, - dayStartingAt(current), effortRemaining); - lastDate = IntraDayDate.create(current.getDate(), - taken.plus(current.getEffortDuration())); + IntraDayDate current = dateFromWhichToAllocate; + while (effortRemaining.compareTo(zero()) > 0) { + PartialDay day = calculateDay(current); + taken = assignForDay(resourcesPerDayModification, day, + effortRemaining); effortRemaining = effortRemaining.minus(taken); + if (effortRemaining.compareTo(zero()) > 0) { + current = nextDay(current); + } } - if (!resourcesPerDayModification.thereAreMoreSpaceAvailableAt(lastDate)) { - return lastDate.nextDayAtStart(); + IntraDayDate result; + if (isForwardScheduling()) { + result = plusEffort(current, taken); + if (!resourcesPerDayModification + .thereAreMoreSpaceAvailableAt(result)) { + result = nextDay(result); + } + } else { + result = minusEffort(current, taken, resourcesPerDayModification); + } + return result; + } + + private IntraDayDate nextDay(IntraDayDate current) { + if (isForwardScheduling()) { + return current.nextDayAtStart(); + } else { + if (current.isStartOfDay()) { + return current.previousDayAtStart(); + } else { + return IntraDayDate.startOfDay(current.getDate()); + } + } + } + + private PartialDay calculateDay(IntraDayDate current) { + if (isForwardScheduling()) { + return dayStartingAt(current); + } else { + return dayEndingAt(current); } - return lastDate; } private PartialDay dayStartingAt(IntraDayDate start) { - return new PartialDay(start, start.nextDayAtStart()); + return new PartialDay(start, nextDay(start)); } - private void setAssignmentsForEachAllocation(IntraDayDate end) { + private PartialDay dayEndingAt(IntraDayDate current) { + if (!current.isStartOfDay()) { + return new PartialDay(IntraDayDate.startOfDay(current.getDate()), + current); + } + return PartialDay.wholeDay(current.getDate().minusDays(1)); + } + + protected boolean isForwardScheduling() { + return Direction.FORWARD == direction; + } + + private IntraDayDate plusEffort(IntraDayDate current, EffortDuration taken) { + return IntraDayDate.create(current.getDate(), + taken.plus(current.getEffortDuration())); + } + + private IntraDayDate minusEffort(IntraDayDate current, + EffortDuration taken, + ResourcesPerDayModification resourcesPerDayModification) { + if (!current.isStartOfDay()) { + return IntraDayDate.create(current.getDate(), current + .getEffortDuration().minus(taken)); + } else { + LocalDate day = current.getDate().minusDays(1); + EffortDuration effortAtDay = resourcesPerDayModification + .durationAtDay(PartialDay.wholeDay(day)); + return IntraDayDate.create(day, effortAtDay.minus(taken)); + } + } + + private void setAssignmentsForEachAllocation(IntraDayDate resultDate) { for (Entry> entry : resultAssignments .entrySet()) { - setNewDataForAllocation(entry, end); + setNewDataForAllocation(entry, resultDate); } } private void setNewDataForAllocation( Entry> entry, - IntraDayDate end) { + IntraDayDate resultDate) { @SuppressWarnings("unchecked") ResourceAllocation allocation = (ResourceAllocation) entry .getKey().getBeingModified(); ResourcesPerDay resourcesPerDay = entry.getKey().getGoal(); @SuppressWarnings("unchecked") List value = (List) entry.getValue(); - setNewDataForAllocation(allocation, end, resourcesPerDay, + setNewDataForAllocation(allocation, resultDate, resourcesPerDay, value); } + protected Direction getDirection() { + return direction; + } + protected abstract void setNewDataForAllocation( - ResourceAllocation allocation, IntraDayDate explicitEnd, + ResourceAllocation allocation, IntraDayDate resultDate, ResourcesPerDay resourcesPerDay, List dayAssignments); protected abstract List createAssignmentsAtDay( @@ -152,7 +230,7 @@ public abstract class UntilFillingHoursAllocator { EffortDuration limit); protected abstract CapacityResult thereAreAvailableHoursFrom( - IntraDayDate start, + IntraDayDate dateFromWhichToAllocate, ResourcesPerDayModification resourcesPerDayModification, EffortDuration remainingDuration); @@ -205,13 +283,13 @@ public abstract class UntilFillingHoursAllocator { } public List calculateEffortsPerAllocation( - IntraDayDate start, EffortDuration toAssign) { + IntraDayDate dateFromWhichToAllocate, EffortDuration toAssign) { do { List durations = divideEffort(toAssign); List result = EffortPerAllocation.wrap( allocations, durations); List unsatisfied = getUnsatisfied( - start, result); + dateFromWhichToAllocate, result); if (unsatisfied.isEmpty()) { return result; } @@ -221,13 +299,12 @@ public abstract class UntilFillingHoursAllocator { } private List getUnsatisfied( - IntraDayDate start, + IntraDayDate dateFromWhichToAllocate, List hoursPerAllocations) { List cannotSatisfy = new ArrayList(); for (EffortPerAllocation each : hoursPerAllocations) { CapacityResult capacityResult = thereAreAvailableHoursFrom( - start, each.allocation, - each.duration); + dateFromWhichToAllocate, each.allocation, each.duration); if (!capacityResult.thereIsCapacityAvailable()) { cannotSatisfy.add(each.allocation); markUnsatisfied(each.allocation, capacityResult); diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/workingday/IntraDayDate.java b/navalplanner-business/src/main/java/org/navalplanner/business/workingday/IntraDayDate.java index 91e0b2a32..3fbb9367e 100644 --- a/navalplanner-business/src/main/java/org/navalplanner/business/workingday/IntraDayDate.java +++ b/navalplanner-business/src/main/java/org/navalplanner/business/workingday/IntraDayDate.java @@ -341,4 +341,8 @@ public class IntraDayDate implements Comparable { return IntraDayDate.startOfDay(getDate().plusDays(1)); } + public IntraDayDate previousDayAtStart() { + return IntraDayDate.startOfDay(getDate().minusDays(1)); + } + } diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/UntilFillingHoursAllocatorTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/UntilFillingHoursAllocatorTest.java index 57bf6ca2f..bc4545fe9 100644 --- a/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/UntilFillingHoursAllocatorTest.java +++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/UntilFillingHoursAllocatorTest.java @@ -50,6 +50,7 @@ import org.navalplanner.business.calendars.entities.ThereAreHoursOnWorkHoursCalc import org.navalplanner.business.planner.entities.GenericResourceAllocation; import org.navalplanner.business.planner.entities.ResourceAllocation; import org.navalplanner.business.planner.entities.ResourceAllocation.AllocationsSpecified.INotFulfilledReceiver; +import org.navalplanner.business.planner.entities.ResourceAllocation.Direction; import org.navalplanner.business.planner.entities.SpecificResourceAllocation; import org.navalplanner.business.planner.entities.Task; import org.navalplanner.business.planner.entities.allocationalgorithms.ResourcesPerDayModification; @@ -75,6 +76,8 @@ public class UntilFillingHoursAllocatorTest { private Integer initialLengthDaysForTask; + private IntraDayDate endDate; + @Test(expected = IllegalArgumentException.class) public void allTasksOfAllocationsMustBeNotNull() { givenAllocationsWithoutTask(); @@ -131,6 +134,41 @@ public class UntilFillingHoursAllocatorTest { assertThat(allocation.getAssignments(), haveHours(16, 16)); } + @Test + public void theAllocationCanBeDoneFromEnd() { + givenStartDate(IntraDayDate.startOfDay(new LocalDate(2009, 1, 10))); + givenTaskOfDaysLength(10);// so end is day 20 + givenSpecificAllocations(ResourcesPerDay.amount(1)); + IntraDayDate newStart = ResourceAllocation.allocating(allocations) + .untilAllocating(Direction.BACKWARD, 16); + assertThat(newStart, + equalTo(IntraDayDate.startOfDay(new LocalDate(2009, 1, 18)))); + } + + @Test + public void theAllocationCanBeDoneFromAnEndThatIsInTheMiddleOfTheDay() { + givenStartDate(IntraDayDate + .create(new LocalDate(2009, 1, 10), hours(4))); + givenEndDate(IntraDayDate.create(new LocalDate(2009, 1, 19), hours(2))); + givenSpecificAllocations(ResourcesPerDay.amount(1)); + IntraDayDate newStart = ResourceAllocation.allocating(allocations) + .untilAllocating(Direction.BACKWARD, 10); + assertThat(newStart, + equalTo(IntraDayDate.startOfDay(new LocalDate(2009, 1, 18)))); + + } + + @Test + public void theAllocationCanBeDoneFromEndAndTheStartDateIsCorrectlyCalculatedIfTheLastDayDoesntTakeAll() { + givenStartDate(IntraDayDate.startOfDay(new LocalDate(2009, 1, 10))); + givenTaskOfDaysLength(10);// so end is day 20 + givenSpecificAllocations(ResourcesPerDay.amount(1)); + IntraDayDate newStart = ResourceAllocation.allocating(allocations) + .untilAllocating(Direction.BACKWARD, 14); + assertThat(newStart, equalTo(IntraDayDate.create(new LocalDate(2009, 1, + 18), hours(2)))); + } + @Test public void ifNoAvailableHoursTheAllocationsAreNotSatisfied() { AvailabilityTimeLine availability = AvailabilityTimeLine.allValid(); @@ -385,6 +423,10 @@ public class UntilFillingHoursAllocatorTest { this.startDate = start; } + private void givenEndDate(IntraDayDate end) { + this.endDate = end; + } + private void createTaskIfNotCreatedYet() { if (task != null) { return; @@ -394,7 +436,9 @@ public class UntilFillingHoursAllocatorTest { startDate = IntraDayDate.startOfDay(new LocalDate(2009, 10, 10)); } IntraDayDate end = null; - if (initialLengthDaysForTask != null) { + if (this.endDate != null) { + end = endDate; + } else if (initialLengthDaysForTask != null) { LocalDate startPlusDays = startDate.getDate().plusDays( initialLengthDaysForTask); end = IntraDayDate.startOfDay(startPlusDays);