/* * 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; import static org.libreplan.business.i18n.I18nHelper._; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.validation.Valid; import javax.validation.constraints.AssertTrue; import org.apache.commons.lang3.Validate; import org.joda.time.Days; import org.joda.time.LocalDate; /** * Assignment function by stretches. * * @author Diego Pino García <dpino@igalia.com> * @author Manuel Rego Casasnovas <mrego@igalia.com> */ public class StretchesFunction extends AssignmentFunction { public static class Interval { private LocalDate start; private LocalDate end; private final BigDecimal loadProportion; private boolean consolidated = false; public static Interval create(BigDecimal loadProportion, LocalDate start, LocalDate end, boolean consolidated) { Interval result = create(loadProportion, start, end); result.consolidated(consolidated); return result; } public static Interval create(BigDecimal loadProportion, LocalDate start, LocalDate end) { return new Interval(loadProportion, start, end); } public Interval(BigDecimal loadProportion, LocalDate start, LocalDate end) { Validate.notNull(loadProportion); Validate.isTrue(loadProportion.signum() >= 0); Validate.notNull(end); this.loadProportion = loadProportion.setScale(2, RoundingMode.HALF_UP); this.start = start; this.end = end; } public static double[] getHoursPointsFor(int totalHours, List<Interval> intervalsDefinedByStretches) { double[] result = new double[intervalsDefinedByStretches.size()]; int i = 0; int accumulated = 0; for (Interval each : intervalsDefinedByStretches) { accumulated += each.getHoursFor(totalHours); result[i++] = accumulated; } return result; } public static double[] getDayPointsFor(LocalDate start, List<Interval> intervalsDefinedByStretches) { double[] result = new double[intervalsDefinedByStretches.size()]; int i = 0; for (Interval each : intervalsDefinedByStretches) { result[i++] = Days.daysBetween(start, each.getEnd()).getDays(); } return result; } public LocalDate getEnd() { return end; } public BigDecimal getLoadProportion() { return loadProportion; } public boolean hasNoStart() { return start == null; } public LocalDate getStart() { return start; } public int getHoursFor(int totalHours) { return loadProportion.multiply(new BigDecimal(totalHours)).intValue(); } public LocalDate getStartFor(LocalDate allocationStart) { return hasNoStart() ? allocationStart : start; } private void apply(ResourceAllocation<?> resourceAllocation, LocalDate startInclusive, int intervalHours) { Validate.isTrue(!isConsolidated()); resourceAllocation.withPreviousAssociatedResources() .onInterval(getStartFor(startInclusive), getEnd()).allocateHours(intervalHours); } public static void apply(ResourceAllocation<?> allocation, List<Interval> intervalsDefinedByStretches, LocalDate allocationStart, int totalHours) { if ( intervalsDefinedByStretches.isEmpty() ) { return; } Validate.isTrue(totalHours == allocation.getNonConsolidatedHours()); int[] hoursPerInterval = getHoursPerInterval(intervalsDefinedByStretches, totalHours); int remainder = totalHours - sum(hoursPerInterval); hoursPerInterval[0] += remainder; int i = 0; for (Interval interval : intervalsDefinedByStretches) { interval.apply(allocation, allocationStart, hoursPerInterval[i++]); } } private static int[] getHoursPerInterval(List<Interval> intervalsDefinedByStretches, int totalHours) { int[] hoursPerInterval = new int[intervalsDefinedByStretches.size()]; int i = 0; for (Interval each : intervalsDefinedByStretches) { hoursPerInterval[i++] = each.getHoursFor(totalHours); } return hoursPerInterval; } private static int sum(int[] hoursPerInterval) { int result = 0; for (int each : hoursPerInterval) { result += each; } return result; } public String toString() { return String.format("[%s, %s]: %s ", start, end, loadProportion); } public void consolidated(boolean value) { consolidated = value; } public boolean isConsolidated() { return consolidated; } } private List<Stretch> stretches = new ArrayList<>(); private StretchesFunctionTypeEnum type; // Transient field. Not stored private StretchesFunctionTypeEnum desiredType; // Transient. Calculated from resourceAllocation private Stretch consolidatedStretch; // Transient. Used to calculate stretches dates private ResourceAllocation<?> resourceAllocation; public static StretchesFunction create() { return create(new StretchesFunction()); } /** * Constructor for hibernate. Do not use! */ protected StretchesFunction() { } public static List<Interval> intervalsFor(ResourceAllocation<?> allocation, Collection<? extends Stretch> stretches) { ArrayList<Interval> result = new ArrayList<>(); LocalDate previous = null; LocalDate stretchDate; BigDecimal sumOfProportions = BigDecimal.ZERO; BigDecimal loadedProportion; for (Stretch each : stretches) { stretchDate = each.getDateIn(allocation); loadedProportion = each.getAmountWorkPercentage().subtract(sumOfProportions); if ( loadedProportion.signum() < 0 ) { loadedProportion = BigDecimal.ZERO; } result.add(Interval.create(loadedProportion, previous, stretchDate, each.isConsolidated())); sumOfProportions = each.getAmountWorkPercentage(); previous = stretchDate; } return result; } private static <T> T last(List<? extends T> list) { return list.get(list.size() - 1); } public StretchesFunction copy() { StretchesFunction result = StretchesFunction.create(); result.resetToStretchesFrom(this); result.type = type; result.desiredType = desiredType; result.consolidatedStretch = consolidatedStretch; result.resourceAllocation = resourceAllocation; return result; } public void resetToStretchesFrom(StretchesFunction from) { this.removeAllStretches(); for (Stretch each : from.getStretchesDefinedByUser()) { this.addStretch(Stretch.copy(each)); } this.consolidatedStretch = from.consolidatedStretch; } public List<Stretch> getStretchesDefinedByUser() { return Collections.unmodifiableList(Stretch.sortByLengthPercentage(stretches)); } @Valid public List<Stretch> getStretches() { List<Stretch> result = new ArrayList<>(); result.add(getFirstStretch()); result.addAll(stretches); result.add(getLastStretch()); return Collections.unmodifiableList(Stretch.sortByLengthPercentage(result)); } private Stretch getLastStretch() { Stretch result = Stretch.create(BigDecimal.ONE, BigDecimal.ONE); result.readOnly(true); return result; } private Stretch getFirstStretch() { Stretch result = Stretch.create(BigDecimal.ZERO, BigDecimal.ZERO); result.readOnly(true); return result; } public StretchesFunctionTypeEnum getType() { return type == null ? StretchesFunctionTypeEnum.STRETCHES : type; } public StretchesFunctionTypeEnum getDesiredType() { return desiredType == null ? getType() : desiredType; } public void changeTypeTo(StretchesFunctionTypeEnum type) { desiredType = type; } public void addStretch(Stretch stretch) { stretches.add(stretch); } public void removeStretch(Stretch stretch) { stretches.remove(stretch); } public void removeAllStretches() { stretches.clear(); } @AssertTrue(message = "At least one stretch is needed") public boolean isNoEmptyConstraint() { // first 0%-0% and last 100%-100% stretches are added automatically return getStretchesPlusConsolidated().size() > 2; } @AssertTrue(message = "A stretch has lower or equal values than the previous stretch") public boolean isStretchesOrderConstraint() { List<Stretch> stretchesPlusConsolidated = getStretchesPlusConsolidated(); if ( stretchesPlusConsolidated.isEmpty() ) { return false; } Iterator<Stretch> iterator = stretchesPlusConsolidated.iterator(); Stretch previous = iterator.next(); while (iterator.hasNext()) { Stretch current = iterator.next(); if ( current.getLengthPercentage().compareTo(previous.getLengthPercentage()) <= 0 ) { return false; } if ( current.getAmountWorkPercentage().compareTo(previous.getAmountWorkPercentage()) <= 0 ) { return false; } previous = current; } return true; } public List<Stretch> getStretchesPlusConsolidated() { List<Stretch> result = new ArrayList<>(); result.addAll(getStretches()); if ( consolidatedStretch != null ) { result.add(consolidatedStretch); } return Collections.unmodifiableList(Stretch.sortByLengthPercentage(result)); } @AssertTrue(message = "Last stretch should have one hundred percent " + "length and one hundred percent of work percentage") public boolean isOneHundredPercentConstraint() { List<Stretch> stretches = getStretchesPlusConsolidated(); if ( stretches.isEmpty() ) { return false; } Stretch lastStretch = stretches.get(stretches.size() - 1); if ( lastStretch.getLengthPercentage().compareTo(BigDecimal.ONE) != 0 ) { return false; } if ( lastStretch.getAmountWorkPercentage().compareTo(BigDecimal.ONE) != 0 ) { return false; } return true; } @Override public void applyTo(ResourceAllocation<?> resourceAllocation) { if ( !resourceAllocation.hasAssignments() ) { return; } // Is 100% consolidated if ( resourceAllocation.getFirstNonConsolidatedDate() == null ) { return; } this.resourceAllocation = resourceAllocation; getDesiredType().applyTo(resourceAllocation, this); type = getDesiredType(); } @Override public String getName() { return StretchesFunctionTypeEnum.INTERPOLATED.equals(type) ? AssignmentFunctionName.INTERPOLATION.toString() : AssignmentFunctionName.STRETCHES.toString(); } public List<Interval> getIntervalsDefinedByStretches() { List<Stretch> stretches = stretchesFor(); if ( stretches.isEmpty() ) { return Collections.emptyList(); } checkStretchesSumOneHundredPercent(); return intervalsFor(resourceAllocation, stretches); } private List<Stretch> stretchesFor() { return getDesiredType().equals(StretchesFunctionTypeEnum.INTERPOLATED) ? getStretchesPlusConsolidated() : getStretches(); } private void checkStretchesSumOneHundredPercent() { List<Stretch> stretches = getStretchesPlusConsolidated(); BigDecimal sumOfProportions = stretches.isEmpty() ? BigDecimal.ZERO : last(stretches).getAmountWorkPercentage(); BigDecimal left = calculateLeftFor(sumOfProportions); if ( !left.equals(BigDecimal.ZERO) ) { throw new IllegalStateException(_("Stretches must sum 100%")); } } private BigDecimal calculateLeftFor(BigDecimal sumOfProportions) { BigDecimal left = BigDecimal.ONE.subtract(sumOfProportions); left = left.signum() <= 0 ? BigDecimal.ZERO : left; return left; } public boolean checkHasAtLeastTwoStretches() { return getStretchesPlusConsolidated().size() >= 2; } public boolean isInterpolated() { return getDesiredType().equals(StretchesFunctionTypeEnum.INTERPOLATED); } public void setConsolidatedStretch(Stretch stretch) { consolidatedStretch = stretch; } public Stretch getConsolidatedStretch() { return consolidatedStretch; } public void setResourceAllocation(ResourceAllocation<?> resourceAllocation) { this.resourceAllocation = resourceAllocation; } @Override public boolean isManual() { return false; } }