org.libreplan.business.planner.entities.StretchesFunction.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.business.planner.entities.StretchesFunction.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundacin para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolxico 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 Garca <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;
    }

}