First tries to assign all possible hours without using overtime

If overtime must be used it's distributed evenly among all users using
previous algorithm.

FEA: ItEr71S07FragmentationDeletionItEr70S09
This commit is contained in:
Óscar González Fernández 2011-02-23 18:21:57 +01:00
parent 5e24f6b5b0
commit 4dd213706d
4 changed files with 378 additions and 25 deletions

View file

@ -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);
}
/**
* <p>
* Is the provided duration below the allowed duration? In that case there
* is still spare space for more allocations.
* </p>
* <p>
* 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.
* </p>
*
* @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));
}
}
}

View file

@ -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<ResourceWithAssignedDuration> withoutOvertime) {
EffortDuration result = EffortDuration.zero();
for (ResourceWithAssignedDuration each : withoutOvertime) {
result = result.plus(each.duration);
}
return result;
}
static Map<Resource, ResourceWithAssignedDuration> byResource(
Collection<? extends ResourceWithAssignedDuration> durations) {
Map<Resource, ResourceWithAssignedDuration> result = new HashMap<Resource, ResourceWithAssignedDuration>();
for (ResourceWithAssignedDuration each : durations) {
result.put(each.resource, each);
}
return result;
}
public static IAssignedEffortForResource sumAssignedEffort(
List<ResourceWithAssignedDuration> durations,
final IAssignedEffortForResource assignedEffortForResource) {
final Map<Resource, ResourceWithAssignedDuration> 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<ResourceWithAssignedDuration> join(
Collection<? extends ResourceWithAssignedDuration> a,
Collection<ResourceWithAssignedDuration> b) {
Map<Resource, ResourceWithAssignedDuration> result = byResource(a);
Map<Resource, ResourceWithAssignedDuration> byResource = byResource(b);
for (Entry<Resource, ResourceWithAssignedDuration> 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<ResourceWithAssignedDuration>(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<ResourceWithAvailableCapacity> {
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<ResourceWithDerivedData> resources;
@ -161,25 +267,176 @@ public class EffortDistributor {
}
public List<ResourceWithAssignedDuration> distributeForDay(LocalDate day,
public List<ResourceWithAssignedDuration> distributeForDay(LocalDate date,
EffortDuration totalDuration) {
List<ResourceWithDerivedData> resourcesAssignable = resourcesAssignableAt(day);
List<ShareSource> shares = divisionAt(resourcesAssignable, day);
List<ResourceWithDerivedData> resourcesAssignable = resourcesAssignableAt(date);
List<ResourceWithAssignedDuration> withoutOvertime = assignAllPossibleWithoutOvertime(
date, totalDuration, resourcesAssignable);
EffortDuration remaining = totalDuration
.minus(ResourceWithAssignedDuration
.sumDurations(withoutOvertime));
if (remaining.isZero()) {
return withoutOvertime;
}
List<ResourceWithAssignedDuration> withOvertime = distributeInOvertimeForDayRemainingEffort(
date, remaining,
ResourceWithAssignedDuration.sumAssignedEffort(withoutOvertime,
assignedEffortForResource), resourcesAssignable);
return ResourceWithAssignedDuration.join(withoutOvertime, withOvertime);
}
private List<ResourceWithDerivedData> resourcesAssignableAt(LocalDate day) {
List<ResourceWithDerivedData> result = new ArrayList<ResourceWithDerivedData>();
for (ResourceWithDerivedData each : resources) {
if (resourceSelector.isSelectable(each.resource, day)) {
result.add(each);
}
}
return result;
}
private List<ResourceWithAssignedDuration> assignAllPossibleWithoutOvertime(
LocalDate date, EffortDuration totalDuration,
List<ResourceWithDerivedData> resourcesAssignable) {
List<ResourceWithAvailableCapacity> fromMoreToLessCapacity = resourcesFromMoreToLessCapacityAvailable(
resourcesAssignable, date);
EffortDuration remaining = totalDuration;
List<ResourceWithAssignedDuration> result = new ArrayList<ResourceWithAssignedDuration>();
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<ResourceWithAvailableCapacity> resourcesFromMoreToLessCapacityAvailable(
List<ResourceWithDerivedData> resourcesAssignable, LocalDate date) {
List<ResourceWithAvailableCapacity> result = new ArrayList<ResourceWithAvailableCapacity>();
for (ResourceWithDerivedData each : resourcesAssignable) {
result.add(each.withAvailableCapacityOn(date,
assignedEffortForResource));
}
Collections.sort(result, Collections.reverseOrder());
return result;
}
private List<ResourceWithAssignedDuration> distributeInOvertimeForDayRemainingEffort(
LocalDate day, EffortDuration remainingDuration,
IAssignedEffortForResource assignedEffortForEachResource,
List<ResourceWithDerivedData> assignableResources) {
List<ResourceWithAssignedDuration> 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<ResourceWithDerivedData> resourcesWithAvailableOvertime = withAvailableCapacity(day, newEffortForEachResource, assignableResources);
if (resourcesWithAvailableOvertime.isEmpty()) {
return remainingDistribution;
}
return ResourceWithAssignedDuration.join(
remainingDistribution,
distributeInOvertimeForDayRemainingEffort(day, newRemaining,
newEffortForEachResource,
resourcesWithAvailableOvertime));
}
private List<ResourceWithAssignedDuration> suppressOverAssignationBeyondAvailableCapacity(
LocalDate date,
IAssignedEffortForResource assignedEffortForEachResource,
List<ResourceWithAssignedDuration> resources) {
List<ResourceWithAssignedDuration> result = new ArrayList<ResourceWithAssignedDuration>();
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<ResourceWithDerivedData> withAvailableCapacity(LocalDate date,
IAssignedEffortForResource assignedEffortForEachResource,
List<ResourceWithDerivedData> assignableResources) {
List<ResourceWithDerivedData> result = new ArrayList<ResourceWithDerivedData>();
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<ResourceWithAssignedDuration> distributeRemaining(
LocalDate day, EffortDuration remainingDuration,
IAssignedEffortForResource assignedEffortForEachResource,
List<ResourceWithDerivedData> resourcesWithAvailableOvertime) {
List<ShareSource> 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<ShareSource> divisionAt(
List<ResourceWithDerivedData> resources, LocalDate day) {
List<ResourceWithDerivedData> resources,
IAssignedEffortForResource assignedEffortForEachResource,
LocalDate day) {
List<ShareSource> result = new ArrayList<ShareSource>();
for (int i = 0; i < resources.size(); i++) {
List<Share> shares = new ArrayList<Share>();
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<ResourceWithDerivedData> resourcesAssignableAt(LocalDate day) {
List<ResourceWithDerivedData> result = new ArrayList<ResourceWithDerivedData>();
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;
}

View file

@ -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)));
}
}

View file

@ -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<GenericDayAssignment> assignmentsWorker1 = genericResourceAllocation
.getOrderedAssignmentsFor(worker1);
assertThat(assignmentsWorker1, haveHours(3, 3, 3, 3));
assertThat(assignmentsWorker1, haveHours(1, 1, 1, 1));
List<GenericDayAssignment> assignmentsWorker2 = genericResourceAllocation
.getOrderedAssignmentsFor(worker2);
assertThat(assignmentsWorker2, haveHours(0, 0, 0, 0));
assertThat(assignmentsWorker2, haveHours());
List<GenericDayAssignment> 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<GenericDayAssignment> assignmentsWorker1 = genericResourceAllocation
.getOrderedAssignmentsFor(worker1);
assertThat(assignmentsWorker1, haveHours(10, 10, 10, 10));
List<GenericDayAssignment> assignmentsWorker2 = genericResourceAllocation
.getOrderedAssignmentsFor(worker2);
assertThat(assignmentsWorker2, haveHours(10, 10, 10, 10));
List<GenericDayAssignment> 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<GenericDayAssignment> assignmentsWorker1 = genericResourceAllocation
.getOrderedAssignmentsFor(worker1);
assertThat(assignmentsWorker1, haveHours(8, 8, 8, 8));
List<GenericDayAssignment> 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<GenericDayAssignment> assignmentsWorker3 = genericResourceAllocation
.getOrderedAssignmentsFor(worker3);
assertThat(assignmentsWorker3, haveHours(0, 0, 0, 0));
assertThat(assignmentsWorker3, haveHours());
}
private void givenVirtualWorkerWithCapacityAndLoad(