/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * 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 <http://www.gnu.org/licenses/>. */ package org.libreplan.business.planner.entities.allocationalgorithms; import static org.libreplan.business.workingday.EffortDuration.zero; import java.math.BigDecimal; import java.util.ArrayList; 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.lang3.Validate; import org.joda.time.LocalDate; import org.libreplan.business.calendars.entities.ICalendar; import org.libreplan.business.calendars.entities.ThereAreHoursOnWorkHoursCalculator.CapacityResult; import org.libreplan.business.common.ProportionalDistributor; import org.libreplan.business.planner.entities.DayAssignment; import org.libreplan.business.planner.entities.ResourceAllocation; import org.libreplan.business.planner.entities.ResourceAllocation.Direction; import org.libreplan.business.planner.entities.Task; import org.libreplan.business.util.Pair; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.IntraDayDate; import org.libreplan.business.workingday.IntraDayDate.PartialDay; import org.libreplan.business.workingday.ResourcesPerDay; public abstract class UntilFillingHoursAllocator { private final Direction direction; private final Task task; private List<ResourcesPerDayModification> allocations; private Map<ResourcesPerDayModification, List<DayAssignment>> resultAssignments = new HashMap<ResourcesPerDayModification, List<DayAssignment>>(); public UntilFillingHoursAllocator(Direction direction, Task task, List<ResourcesPerDayModification> allocations) { this.direction = direction; this.task = task; this.allocations = allocations; initializeResultsMap(); } private void initializeResultsMap() { for (ResourcesPerDayModification r : allocations) { resultAssignments.put(r, new ArrayList<DayAssignment>()); } } public IntraDayDate untilAllocating(EffortDuration effortToAllocate) { final IntraDayDate dateFromWhichToAllocate = direction .getDateFromWhichToAllocate(task); List<EffortPerAllocation> effortPerAllocation = effortPerAllocation( dateFromWhichToAllocate, effortToAllocate); if (effortPerAllocation.isEmpty()) { return null; } return untilAllocating(dateFromWhichToAllocate, effortPerAllocation); } private IntraDayDate untilAllocating(final IntraDayDate dateFromWhichToAllocate, List<EffortPerAllocation> effortPerAllocation) { IntraDayDate currentResult = dateFromWhichToAllocate; for (EffortPerAllocation each : effortPerAllocation) { IntraDayDate candidate = untilAllocating(dateFromWhichToAllocate, each.allocation, each.duration); currentResult = pickCurrentOrCandidate(currentResult, candidate); } setAssignmentsForEachAllocation(currentResult); return currentResult; } private IntraDayDate pickCurrentOrCandidate(IntraDayDate current, IntraDayDate candidate) { if (direction == Direction.BACKWARD) { return IntraDayDate.min(current, candidate); } return IntraDayDate.max(current, candidate); } private List<EffortPerAllocation> effortPerAllocation( IntraDayDate dateFromWhichToAllocate, EffortDuration toBeAssigned) { return new HoursPerAllocationCalculator(allocations) .calculateEffortsPerAllocation(dateFromWhichToAllocate, toBeAssigned); } public interface IAssignmentsCreator { List<? extends DayAssignment> createAssignmentsAtDay(PartialDay day, EffortDuration limit, ResourcesPerDay resourcesPerDay); } /** * * @param dateFromWhichToAllocate * @param resourcesPerDayModification * @param effortRemaining * @return the moment on which the allocation would be completed */ private IntraDayDate untilAllocating(IntraDayDate dateFromWhichToAllocate, ResourcesPerDayModification resourcesPerDayModification, EffortDuration effortRemaining) { EffortDuration taken = zero(); EffortDuration biggestLastAssignment = zero(); IntraDayDate current = dateFromWhichToAllocate; IAssignmentsCreator assignmentsCreator = resourcesPerDayModification .createAssignmentsCreator(); while (effortRemaining.compareTo(zero()) > 0) { PartialDay day = calculateDay(current); Pair<EffortDuration, EffortDuration> pair = assignForDay( resourcesPerDayModification, assignmentsCreator, day, effortRemaining); taken = pair.getFirst(); biggestLastAssignment = pair.getSecond(); effortRemaining = effortRemaining.minus(taken); if (effortRemaining.compareTo(zero()) > 0) { current = nextDay(current); } } IntraDayDate finish = adjustFinish(resourcesPerDayModification, taken, biggestLastAssignment, current); // We have to do it now, so the other allocations take it into account. // At the end it's done again with the right end date. setNewDataForAllocation(resourcesPerDayModification, resultAssignments .get(resourcesPerDayModification), finish); return finish; } private IntraDayDate adjustFinish( ResourcesPerDayModification resourcesPerDayModification, EffortDuration allocatedLastDay, EffortDuration biggestLastAssignment, IntraDayDate end) { IntraDayDate result; ResourcesPerDay adjustBy = allocatedLastDay .equals(biggestLastAssignment) ? resourcesPerDayModification .getGoal() : ResourcesPerDay.amount(1); if (isForwardScheduling()) { result = plusEffort(end, allocatedLastDay); if (!resourcesPerDayModification .thereAreMoreSpaceAvailableAt(result)) { result = nextDay(result); } else { result = end.increaseBy(adjustBy, biggestLastAssignment); } } else { result = minusEffort(end, allocatedLastDay, resourcesPerDayModification .getBeingModified().getAllocationCalendar(), adjustBy); } 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); } } private PartialDay dayStartingAt(IntraDayDate start) { return new PartialDay(start, nextDay(start)); } 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, ICalendar calendar, ResourcesPerDay adjustBy) { if (!current.isStartOfDay()) { return current.decreaseBy(adjustBy, taken); } else { LocalDate day = current.getDate().minusDays(1); EffortDuration effortAtDay = calendar .asDurationOn(PartialDay.wholeDay(day), ResourcesPerDay.amount(1)); return IntraDayDate.create(day, effortAtDay).decreaseBy(adjustBy, taken); } } private void setAssignmentsForEachAllocation(IntraDayDate resultDate) { for (Entry<ResourcesPerDayModification, List<DayAssignment>> entry : resultAssignments .entrySet()) { setNewDataForAllocation(entry.getKey(), entry.getValue(), resultDate); } } private <T extends DayAssignment> void setNewDataForAllocation( ResourcesPerDayModification modification, List<T> value, IntraDayDate resultDate) { @SuppressWarnings("unchecked") ResourceAllocation<T> allocation = (ResourceAllocation<T>) modification .getBeingModified(); ResourcesPerDay resourcesPerDay = modification.getGoal(); setNewDataForAllocation(allocation, resultDate, resourcesPerDay, value); } protected Direction getDirection() { return direction; } protected abstract <T extends DayAssignment> void setNewDataForAllocation( ResourceAllocation<T> allocation, IntraDayDate resultDate, ResourcesPerDay resourcesPerDay, List<T> dayAssignments); protected abstract CapacityResult thereAreAvailableHoursFrom( IntraDayDate dateFromWhichToAllocate, ResourcesPerDayModification resourcesPerDayModification, EffortDuration remainingDuration); protected abstract void markUnsatisfied( ResourcesPerDayModification allocationAttempt, CapacityResult capacityResult); /** * * @param resourcesPerDayModification * @return a pair composed of the effort assigned and the biggest assignment * done. */ private Pair<EffortDuration, EffortDuration> assignForDay( ResourcesPerDayModification resourcesPerDayModification, IAssignmentsCreator assignmentsCreator, PartialDay day, EffortDuration remaining) { List<? extends DayAssignment> newAssignments = assignmentsCreator .createAssignmentsAtDay(day, remaining, resourcesPerDayModification.getGoal()); resultAssignments.get(resourcesPerDayModification).addAll( newAssignments); return Pair.create(DayAssignment.sum(newAssignments), getMaxAssignment(newAssignments)); } private EffortDuration getMaxAssignment( List<? extends DayAssignment> newAssignments) { if (newAssignments.isEmpty()) { return zero(); } DayAssignment max = Collections.max(newAssignments, DayAssignment.byDurationComparator()); return max.getDuration(); } private static class EffortPerAllocation { final EffortDuration duration; final ResourcesPerDayModification allocation; private EffortPerAllocation(EffortDuration duration, ResourcesPerDayModification allocation) { this.duration = duration; this.allocation = allocation; } public static List<EffortPerAllocation> wrap( List<ResourcesPerDayModification> allocations, List<EffortDuration> durations) { Validate.isTrue(durations.size() == allocations.size()); int i = 0; List<EffortPerAllocation> result = new ArrayList<EffortPerAllocation>(); for(i = 0; i < allocations.size(); i++){ result.add(new EffortPerAllocation(durations.get(i), allocations.get(i))); } return result; } } private class HoursPerAllocationCalculator { private List<ResourcesPerDayModification> allocations; private HoursPerAllocationCalculator( List<ResourcesPerDayModification> allocations) { this.allocations = new ArrayList<ResourcesPerDayModification>( allocations); } public List<EffortPerAllocation> calculateEffortsPerAllocation( IntraDayDate dateFromWhichToAllocate, EffortDuration toAssign) { do { List<EffortDuration> durations = divideEffort(toAssign); List<EffortPerAllocation> result = EffortPerAllocation.wrap( allocations, durations); List<ResourcesPerDayModification> unsatisfied = getUnsatisfied( dateFromWhichToAllocate, result); if (unsatisfied.isEmpty()) { return result; } allocations.removeAll(unsatisfied); } while (!allocations.isEmpty()); return Collections.emptyList(); } private List<ResourcesPerDayModification> getUnsatisfied( IntraDayDate dateFromWhichToAllocate, List<EffortPerAllocation> hoursPerAllocations) { List<ResourcesPerDayModification> cannotSatisfy = new ArrayList<ResourcesPerDayModification>(); for (EffortPerAllocation each : hoursPerAllocations) { CapacityResult capacityResult = thereAreAvailableHoursFrom( dateFromWhichToAllocate, each.allocation, each.duration); if (!capacityResult.thereIsCapacityAvailable()) { cannotSatisfy.add(each.allocation); markUnsatisfied(each.allocation, capacityResult); } } return cannotSatisfy; } private List<EffortDuration> divideEffort(EffortDuration toBeDivided) { ProportionalDistributor distributor = ProportionalDistributor .create(createShares()); int[] secondsDivided = distributor.distribute(toBeDivided .getSeconds()); return asDurations(secondsDivided); } private int[] createShares() { int[] result = new int[allocations.size()]; for (int i = 0; i < result.length; i++) { result[i] = normalize(allocations.get(i).getGoal() .getAmount()); } return result; } private List<EffortDuration> asDurations(int[] secondsDivided) { List<EffortDuration> result = new ArrayList<EffortDuration>(); for (int each : secondsDivided) { result.add(EffortDuration.seconds(each)); } return result; } /** * Returns a normalized amount for {@link ProportionalDistributor}. For * example, for 2.03, 203 is returned. * * @param amount * @return */ private int normalize(BigDecimal amount) { return amount.movePointRight(2).intValue(); } } }