diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/Capacity.java b/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/Capacity.java
index ee440f4ba..5ca66bce0 100644
--- a/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/Capacity.java
+++ b/navalplanner-business/src/main/java/org/navalplanner/business/calendars/entities/Capacity.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang.builder.HashCodeBuilder;
import org.navalplanner.business.workingday.EffortDuration;
import org.navalplanner.business.workingday.EffortDuration.Granularity;
+
/**
* This class is intended as a Hibernate component. It's formed by two
* components, the standard effort and the allowed extra effort. It represents
@@ -180,6 +181,28 @@ public class Capacity {
duration);
}
+ /**
+ *
+ * Is the provided duration below the allowed duration? In that case there
+ * is still spare space for more allocations.
+ *
+ *
+ * The allowed duration is infinite if this {@link Capacity} is
+ * {@link #overAssignableWithoutLimit(boolean)} or the duration provided is
+ * less than the sum of the standard plus allowed extra effort.
+ *
+ *
+ * @param assignedDuration
+ * @return
+ */
+ public boolean hasSpareSpaceForMoreAllocations(
+ EffortDuration assignedDuration) {
+ Validate.notNull(assignedDuration);
+ return isOverAssignableWithoutLimit()
+ || assignedDuration.compareTo(standardEffort
+ .plus(allowedExtraEffort)) < 0;
+ }
+
public boolean allowsWorking() {
return !getStandardEffort().isZero() || isOverAssignableWithoutLimit()
|| !getAllowedExtraEffort().isZero();
@@ -192,4 +215,4 @@ public class Capacity {
: allowedExtraEffort.multiplyBy(capacity));
}
-}
+}
\ No newline at end of file
diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/EffortDistributor.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/EffortDistributor.java
index 4d59a7d1a..03169bf06 100644
--- a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/EffortDistributor.java
+++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/EffortDistributor.java
@@ -25,10 +25,15 @@ import static org.navalplanner.business.workingday.EffortDuration.seconds;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+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.Capacity;
import org.navalplanner.business.calendars.entities.ICalendar;
import org.navalplanner.business.calendars.entities.ResourceCalendar;
import org.navalplanner.business.calendars.entities.SameWorkHoursEveryDay;
@@ -88,6 +93,68 @@ public class EffortDistributor {
this.duration = duration;
this.resource = resource;
}
+
+ public static EffortDuration sumDurations(
+ List withoutOvertime) {
+ EffortDuration result = EffortDuration.zero();
+ for (ResourceWithAssignedDuration each : withoutOvertime) {
+ result = result.plus(each.duration);
+ }
+ return result;
+ }
+
+ static Map byResource(
+ Collection extends ResourceWithAssignedDuration> durations) {
+ Map result = new HashMap();
+ for (ResourceWithAssignedDuration each : durations) {
+ result.put(each.resource, each);
+ }
+ return result;
+ }
+
+ public static IAssignedEffortForResource sumAssignedEffort(
+ List durations,
+ final IAssignedEffortForResource assignedEffortForResource) {
+ final Map byResource = byResource(durations);
+ return new IAssignedEffortForResource() {
+
+ @Override
+ public EffortDuration getAssignedDurationAt(Resource resource,
+ LocalDate day) {
+ EffortDuration previouslyAssigned = assignedEffortForResource
+ .getAssignedDurationAt(resource, day);
+ ResourceWithAssignedDuration withDuration = byResource
+ .get(resource);
+ if (withDuration != null) {
+ return previouslyAssigned.plus(withDuration.duration);
+ }
+ return previouslyAssigned;
+ }
+ };
+ }
+
+ public static List join(
+ Collection extends ResourceWithAssignedDuration> a,
+ Collection b) {
+ Map result = byResource(a);
+ Map byResource = byResource(b);
+ for (Entry each : byResource
+ .entrySet()) {
+ Resource key = each.getKey();
+ ResourceWithAssignedDuration value = each.getValue();
+ if (result.containsKey(key)) {
+ result.put(key, result.get(key).plus(value));
+ } else {
+ result.put(key, value);
+ }
+ }
+ return new ArrayList(result.values());
+ }
+
+ ResourceWithAssignedDuration plus(ResourceWithAssignedDuration value) {
+ return new ResourceWithAssignedDuration(
+ this.duration.plus(value.duration), resource);
+ }
}
private static final ICalendar generateCalendarFor(Resource resource) {
@@ -138,6 +205,45 @@ public class EffortDistributor {
this.calendar = generateCalendarFor(resource);
}
+ ResourceWithAvailableCapacity withAvailableCapacityOn(LocalDate date,
+ IAssignedEffortForResource assignedEffort) {
+ EffortDuration capacity = calendar.getCapacityOn(PartialDay
+ .wholeDay(date));
+ EffortDuration assigned = assignedEffort.getAssignedDurationAt(
+ resource, date);
+ EffortDuration available = capacity.compareTo(assigned) > 0 ? capacity
+ .minus(assigned)
+ : EffortDuration.zero();
+ return new ResourceWithAvailableCapacity(resource, available);
+ }
+
+ }
+
+ private static class ResourceWithAvailableCapacity implements
+ Comparable {
+
+ private final Resource resource;
+
+ private final EffortDuration available;
+
+ public ResourceWithAvailableCapacity(Resource resource,
+ EffortDuration available) {
+ Validate.notNull(resource);
+ Validate.notNull(available);
+ this.resource = resource;
+ this.available = available;
+ }
+
+ public ResourceWithAssignedDuration doBiggestAssignationPossible(
+ EffortDuration remaining) {
+ return new ResourceWithAssignedDuration(EffortDuration.min(
+ remaining, available), resource);
+ }
+
+ @Override
+ public int compareTo(ResourceWithAvailableCapacity o) {
+ return available.compareTo(o.available);
+ }
}
private final List resources;
@@ -161,25 +267,176 @@ public class EffortDistributor {
}
- public List distributeForDay(LocalDate day,
+ public List distributeForDay(LocalDate date,
EffortDuration totalDuration) {
- List resourcesAssignable = resourcesAssignableAt(day);
- List shares = divisionAt(resourcesAssignable, day);
+ List resourcesAssignable = resourcesAssignableAt(date);
+ List withoutOvertime = assignAllPossibleWithoutOvertime(
+ date, totalDuration, resourcesAssignable);
+ EffortDuration remaining = totalDuration
+ .minus(ResourceWithAssignedDuration
+ .sumDurations(withoutOvertime));
+ if (remaining.isZero()) {
+ return withoutOvertime;
+ }
+ List withOvertime = distributeInOvertimeForDayRemainingEffort(
+ date, remaining,
+ ResourceWithAssignedDuration.sumAssignedEffort(withoutOvertime,
+ assignedEffortForResource), resourcesAssignable);
+ return ResourceWithAssignedDuration.join(withoutOvertime, withOvertime);
+ }
+
+ private List resourcesAssignableAt(LocalDate day) {
+ List result = new ArrayList();
+ for (ResourceWithDerivedData each : resources) {
+ if (resourceSelector.isSelectable(each.resource, day)) {
+ result.add(each);
+ }
+ }
+ return result;
+ }
+
+ private List assignAllPossibleWithoutOvertime(
+ LocalDate date, EffortDuration totalDuration,
+ List resourcesAssignable) {
+ List fromMoreToLessCapacity = resourcesFromMoreToLessCapacityAvailable(
+ resourcesAssignable, date);
+ EffortDuration remaining = totalDuration;
+ List result = new ArrayList();
+ for (ResourceWithAvailableCapacity each : fromMoreToLessCapacity) {
+ if (!each.available.isZero()) {
+ ResourceWithAssignedDuration r = each
+ .doBiggestAssignationPossible(remaining);
+ remaining = remaining.minus(r.duration);
+ if (!r.duration.isZero()) {
+ result.add(r);
+ }
+ }
+ }
+ return result;
+ }
+
+ private List resourcesFromMoreToLessCapacityAvailable(
+ List resourcesAssignable, LocalDate date) {
+ List result = new ArrayList();
+ for (ResourceWithDerivedData each : resourcesAssignable) {
+ result.add(each.withAvailableCapacityOn(date,
+ assignedEffortForResource));
+ }
+ Collections.sort(result, Collections.reverseOrder());
+ return result;
+ }
+
+ private List distributeInOvertimeForDayRemainingEffort(
+ LocalDate day, EffortDuration remainingDuration,
+ IAssignedEffortForResource assignedEffortForEachResource,
+ List assignableResources) {
+ List remainingDistribution = suppressOverAssignationBeyondAvailableCapacity(
+ day,
+ assignedEffortForEachResource,
+ distributeRemaining(day, remainingDuration,
+ assignedEffortForEachResource, assignableResources));
+
+ EffortDuration durationDistributed = ResourceWithAssignedDuration
+ .sumDurations(remainingDistribution);
+ EffortDuration newRemaining = remainingDuration
+ .minus(durationDistributed);
+ assert newRemaining.compareTo(EffortDuration.zero()) >= 0;
+ if (newRemaining.isZero()) {
+ return remainingDistribution;
+ }
+ IAssignedEffortForResource newEffortForEachResource = ResourceWithAssignedDuration.sumAssignedEffort(
+ remainingDistribution, assignedEffortForEachResource);
+
+ List resourcesWithAvailableOvertime = withAvailableCapacity(day, newEffortForEachResource, assignableResources);
+ if (resourcesWithAvailableOvertime.isEmpty()) {
+ return remainingDistribution;
+ }
+ return ResourceWithAssignedDuration.join(
+ remainingDistribution,
+ distributeInOvertimeForDayRemainingEffort(day, newRemaining,
+ newEffortForEachResource,
+ resourcesWithAvailableOvertime));
+ }
+
+ private List suppressOverAssignationBeyondAvailableCapacity(
+ LocalDate date,
+ IAssignedEffortForResource assignedEffortForEachResource,
+ List resources) {
+ List result = new ArrayList();
+ for (ResourceWithAssignedDuration each : resources) {
+ Resource resource = each.resource;
+ ICalendar calendar = generateCalendarFor(resource);
+ Capacity capacityWithOvertime = calendar
+ .getCapacityWithOvertime(date);
+ if (capacityWithOvertime.isOverAssignableWithoutLimit()) {
+ result.add(each);
+ } else {
+ EffortDuration durationCanBeAdded = calculateDurationCanBeAdded(
+ assignedEffortForEachResource.getAssignedDurationAt(
+ resource, date), capacityWithOvertime,
+ each.duration);
+ if (!durationCanBeAdded.isZero()) {
+ result.add(new ResourceWithAssignedDuration(
+ durationCanBeAdded, resource));
+ }
+ }
+ }
+ return result;
+ }
+
+ private EffortDuration calculateDurationCanBeAdded(
+ EffortDuration alreadyAssigned, Capacity capacityWithOvertime,
+ EffortDuration newAddition) {
+ EffortDuration maximum = capacityWithOvertime.getStandardEffort().plus(
+ capacityWithOvertime.getAllowedExtraEffort());
+ if (alreadyAssigned.compareTo(maximum) >= 0) {
+ return EffortDuration.zero();
+ } else {
+ return EffortDuration.min(newAddition,
+ maximum.minus(alreadyAssigned));
+ }
+ }
+
+ private List withAvailableCapacity(LocalDate date,
+ IAssignedEffortForResource assignedEffortForEachResource,
+ List assignableResources) {
+ List result = new ArrayList();
+ for (ResourceWithDerivedData each : assignableResources) {
+ Capacity capacity = each.calendar.getCapacityWithOvertime(date);
+ EffortDuration assignedEffort = assignedEffortForEachResource
+ .getAssignedDurationAt(each.resource, date);
+ if (capacity.hasSpareSpaceForMoreAllocations(assignedEffort)) {
+ result.add(each);
+ }
+ }
+ return result;
+ }
+
+ private List distributeRemaining(
+ LocalDate day, EffortDuration remainingDuration,
+ IAssignedEffortForResource assignedEffortForEachResource,
+ List resourcesWithAvailableOvertime) {
+ List shares = divisionAt(resourcesWithAvailableOvertime,
+ assignedEffortForEachResource, day);
ShareDivision currentDivision = ShareSource.all(shares);
- ShareDivision newDivison = currentDivision.plus(totalDuration.getSeconds());
+ ShareDivision newDivison = currentDivision.plus(remainingDuration
+ .getSeconds());
int[] differences = currentDivision.to(newDivison);
return ShareSource.durationsForEachResource(shares, differences,
- ResourceWithDerivedData.resources(resourcesAssignable));
+ ResourceWithDerivedData
+ .resources(resourcesWithAvailableOvertime));
}
private List divisionAt(
- List resources, LocalDate day) {
+ List resources,
+ IAssignedEffortForResource assignedEffortForEachResource,
+ LocalDate day) {
List result = new ArrayList();
for (int i = 0; i < resources.size(); i++) {
List shares = new ArrayList();
Resource resource = resources.get(i).resource;
ICalendar calendarForResource = resources.get(i).calendar;
- EffortDuration alreadyAssigned = assignedEffortForResource
+ EffortDuration alreadyAssigned = assignedEffortForEachResource
.getAssignedDurationAt(resource, day);
final int alreadyAssignedSeconds = alreadyAssigned.getSeconds();
Integer capacityEachOneSeconds = calendarForResource.asDurationOn(
@@ -197,16 +454,6 @@ public class EffortDistributor {
return result;
}
- private List resourcesAssignableAt(LocalDate day) {
- List result = new ArrayList();
- for (ResourceWithDerivedData each : resources) {
- if (resourceSelector.isSelectable(each.resource, day)) {
- result.add(each);
- }
- }
- return result;
- }
-
private static final ResourcesPerDay ONE = ResourcesPerDay.amount(1);
private static class ShareSource {
@@ -231,8 +478,11 @@ public class EffortDistributor {
int sum = sumDifferences(differencesInSeconds, differencesIndex,
differencesToTake);
differencesIndex += differencesToTake;
- result.add(new ResourceWithAssignedDuration(seconds(sum),
- resource));
+ ResourceWithAssignedDuration withAssignedDuration = new ResourceWithAssignedDuration(
+ seconds(sum), resource);
+ if (!withAssignedDuration.duration.isZero()) {
+ result.add(withAssignedDuration);
+ }
}
return result;
}
diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/CapacityTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/CapacityTest.java
index a0271bc7e..9b3b3c1a9 100644
--- a/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/CapacityTest.java
+++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/calendars/entities/CapacityTest.java
@@ -19,6 +19,7 @@
package org.navalplanner.business.test.calendars.entities;
import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
@@ -181,4 +182,30 @@ public class CapacityTest {
a.withAllowedExtraEffort(hours(4)))
.getAllowedExtraEffort(), equalTo(hours(4)));
}
+
+ @Test
+ public void testThereIsCapacityForMoreAllocations() {
+ assertThat(Capacity.create(hours(8)).overAssignableWithoutLimit(true)
+ .hasSpareSpaceForMoreAllocations(hours(10)), is(true));
+
+ Capacity notOverassignable = Capacity.create(hours(8))
+ .overAssignableWithoutLimit(false);
+
+ assertFalse(notOverassignable
+ .hasSpareSpaceForMoreAllocations(hours(10)));
+ assertTrue(notOverassignable
+ .hasSpareSpaceForMoreAllocations(hours(7)));
+ assertFalse(notOverassignable
+ .hasSpareSpaceForMoreAllocations(hours(8)));
+
+ Capacity withSomeExtraHours = notOverassignable
+ .withAllowedExtraEffort(hours(2));
+
+ assertFalse(withSomeExtraHours
+ .hasSpareSpaceForMoreAllocations(hours(10)));
+ assertTrue(withSomeExtraHours
+ .hasSpareSpaceForMoreAllocations(hours(7)));
+ assertTrue(withSomeExtraHours
+ .hasSpareSpaceForMoreAllocations(hours(8)));
+ }
}
diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/GenericResourceAllocationTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/GenericResourceAllocationTest.java
index fd413f7f9..be3c3b852 100644
--- a/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/GenericResourceAllocationTest.java
+++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/GenericResourceAllocationTest.java
@@ -21,6 +21,7 @@
package org.navalplanner.business.test.planner.entities;
+import static java.util.Arrays.asList;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.getCurrentArguments;
import static org.easymock.EasyMock.isA;
@@ -512,7 +513,7 @@ public class GenericResourceAllocationTest {
}
@Test
- public void moreBusyResourcesAreGivenLessLoad() {
+ public void itGivesAllTheLoadItCanToTheLessLoadedResource() {
final int TASK_DURATION_DAYS = 4;
givenBaseCalendarWithoutExceptions(8);
LocalDate start = new LocalDate(2006, 10, 5);
@@ -526,13 +527,65 @@ public class GenericResourceAllocationTest {
List assignmentsWorker1 = genericResourceAllocation
.getOrderedAssignmentsFor(worker1);
- assertThat(assignmentsWorker1, haveHours(3, 3, 3, 3));
+ assertThat(assignmentsWorker1, haveHours(1, 1, 1, 1));
List assignmentsWorker2 = genericResourceAllocation
.getOrderedAssignmentsFor(worker2);
- assertThat(assignmentsWorker2, haveHours(0, 0, 0, 0));
+ assertThat(assignmentsWorker2, haveHours());
List assignmentsWorker3 = genericResourceAllocation
.getOrderedAssignmentsFor(worker3);
- assertThat(assignmentsWorker3, haveHours(5, 5, 5, 5));
+ assertThat(assignmentsWorker3, haveHours(7, 7, 7, 7));
+ }
+
+ @Test
+ public void doesntSurpassTheExtraHours() {
+ final int TASK_DURATION_DAYS = 4;
+ givenBaseCalendarWithoutExceptions(8);
+ LocalDate start = new LocalDate(2006, 10, 5);
+ givenTaskWithStartAndEnd(toInterval(start,
+ Period.days(TASK_DURATION_DAYS)));
+ givenGenericResourceAllocationForTask(task);
+
+ Capacity workingDay = Capacity.create(hours(8));
+ Capacity with2ExtraHours = workingDay
+ .withAllowedExtraEffort(hours(2));
+ givenCalendarsForResources(with2ExtraHours, with2ExtraHours,
+ workingDay.overAssignableWithoutLimit(true));
+ givenWorkersWithLoads(0, 0, 0);
+
+ genericResourceAllocation.forResources(workers).allocate(
+ ResourcesPerDay.amount(4));
+
+ List assignmentsWorker1 = genericResourceAllocation
+ .getOrderedAssignmentsFor(worker1);
+ assertThat(assignmentsWorker1, haveHours(10, 10, 10, 10));
+ List assignmentsWorker2 = genericResourceAllocation
+ .getOrderedAssignmentsFor(worker2);
+ assertThat(assignmentsWorker2, haveHours(10, 10, 10, 10));
+ List assignmentsWorker3 = genericResourceAllocation
+ .getOrderedAssignmentsFor(worker3);
+ assertThat(assignmentsWorker3, haveHours(12, 12, 12, 12));
+
+ }
+
+ @Test
+ public void itGivesAllTheLoadItCanToTheLessLoadedResourceAndThenToTheNextOne() {
+ final int TASK_DURATION_DAYS = 4;
+ givenBaseCalendarWithoutExceptions(8);
+ LocalDate start = new LocalDate(2006, 10, 5);
+ givenTaskWithStartAndEnd(toInterval(start,
+ Period.days(TASK_DURATION_DAYS)));
+ givenGenericResourceAllocationForTask(task);
+ givenWorkersWithLoads(0, 0, 0);
+
+ genericResourceAllocation.forResources(asList(worker1, worker2))
+ .allocate(ResourcesPerDay.amount(2));
+
+ List assignmentsWorker1 = genericResourceAllocation
+ .getOrderedAssignmentsFor(worker1);
+ assertThat(assignmentsWorker1, haveHours(8, 8, 8, 8));
+ List assignmentsWorker2 = genericResourceAllocation
+ .getOrderedAssignmentsFor(worker2);
+ assertThat(assignmentsWorker2, haveHours(8, 8, 8, 8));
}
@Test
@@ -588,7 +641,7 @@ public class GenericResourceAllocationTest {
assertThat(assignmentsWorker2, haveHours(4, 4, 4, 4));
List assignmentsWorker3 = genericResourceAllocation
.getOrderedAssignmentsFor(worker3);
- assertThat(assignmentsWorker3, haveHours(0, 0, 0, 0));
+ assertThat(assignmentsWorker3, haveHours());
}
private void givenVirtualWorkerWithCapacityAndLoad(