From 4dd213706d821b1c9ce505755d11da12c7693c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Gonz=C3=A1lez=20Fern=C3=A1ndez?= Date: Wed, 23 Feb 2011 18:21:57 +0100 Subject: [PATCH] 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 --- .../business/calendars/entities/Capacity.java | 25 +- .../planner/entities/EffortDistributor.java | 288 ++++++++++++++++-- .../test/calendars/entities/CapacityTest.java | 27 ++ .../GenericResourceAllocationTest.java | 63 +++- 4 files changed, 378 insertions(+), 25 deletions(-) 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 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 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(