/*
* This file is part of LibrePlan
*
* 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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.Validate;
import org.joda.time.LocalDate;
import org.libreplan.business.calendars.entities.BaseCalendar;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;
/**
*
* @author Diego Pino Garcia<dpino@igalia.com>
*
*/
public class SigmoidFunction extends AssignmentFunction {
private static final int PRECISSION = 6;
private static final int ROUND_MODE = BigDecimal.ROUND_HALF_EVEN;
// Fragmentation of hours (0.25, 0.50, 0.75, 1). 1 indicates no fragmentation
private static final BigDecimal HOUR_FRAGMENTATION = BigDecimal
.valueOf(0.25);
public static SigmoidFunction create() {
return create(new SigmoidFunction());
}
protected SigmoidFunction() {
}
@Override
public String getName() {
return AssignmentFunctionName.SIGMOID.toString();
}
@Override
public void applyTo(ResourceAllocation<?> resourceAllocation) {
final Task task = resourceAllocation.getTask();
final int totalHours = resourceAllocation.getAssignedHours();
apply(resourceAllocation, task.getStartAsLocalDate(), task.getEndAsLocalDate(), totalHours);
}
private void apply(ResourceAllocation<?> resourceAllocation,
LocalDate start, LocalDate end, int totalHours) {
final LocalDate previousEndDate = resourceAllocation.getEndDate();
EffortDuration capacity;
BaseCalendar calendar = resourceAllocation.getTask().getCalendar();
int daysDuration = daysWithAllocatedHours(resourceAllocation).size();
// Calculate hours per day and round values
BigDecimal[] hoursToAllocatePerDay = generateHoursToAllocateFor(daysDuration, totalHours);
hoursToAllocatePerDay = roundValues(hoursToAllocatePerDay, HOUR_FRAGMENTATION);
// Calculate reminder (difference between totalHours and sum of hours calculated)
BigDecimal totalHoursToAllocate = sumHoursPerDay(hoursToAllocatePerDay);
assert(totalHoursToAllocate.compareTo(BigDecimal.valueOf(totalHours)) <= 0);
BigDecimal remindingHours = BigDecimal.valueOf(totalHours).subtract(totalHoursToAllocate);
allocateRemindingHours(hoursToAllocatePerDay, remindingHours);
avoidZeroHoursInDays(hoursToAllocatePerDay);
assert(hoursToAllocatePerDay.length == daysDuration);
// Starting from startDate do allocation, one slot of hours per day in resource
LocalDate day = new LocalDate(start);
EffortDuration hours = EffortDuration.zero();
int i = 0;
while (i < hoursToAllocatePerDay.length) {
hours = EffortDuration
.fromHoursAsBigDecimal(hoursToAllocatePerDay[i]);
capacity = calendar.getCapacityOn(PartialDay.wholeDay(day));
if (!EffortDuration.zero().equals(capacity)) {
allocate(resourceAllocation, day, hours);
i++;
}
day = day.plusDays(1);
}
Validate.isTrue(resourceAllocation.getEndDate().equals(previousEndDate));
}
private List<BigDecimal> daysWithAllocatedHours(
ResourceAllocation<?> resourceAllocation) {
List<BigDecimal> result = new ArrayList<BigDecimal>();
LocalDate day = new LocalDate(resourceAllocation.getStartDate());
final LocalDate end = resourceAllocation.getEndDate();
while (day.isBefore(end)) {
int hoursAllocated = resourceAllocation.getAssignedHours(day, day.plusDays(1));
if (hoursAllocated != 0) {
result.add(new BigDecimal(hoursAllocated));
}
day = day.plusDays(1);
}
return result;
}
/**
* Days with zero hours can occur at the beginning days.
*
* To avoid allocating days with zero hours, we iterate through the days and
* subtract a day from the next day to the current day, until we come up
* with a day which is no zero
*
* @param hoursToAllocatePerDay
*/
private void avoidZeroHoursInDays(BigDecimal[] hoursToAllocatePerDay) {
int length = hoursToAllocatePerDay.length;
for (int i = 0; i < length; i++) {
BigDecimal hours = hoursToAllocatePerDay[i];
if (hours.doubleValue() != 0) {
return;
}
if (i + 1 <= length) {
BigDecimal next = hoursToAllocatePerDay[i + 1];
hoursToAllocatePerDay[i + 1] = next
.subtract(HOUR_FRAGMENTATION);
hoursToAllocatePerDay[i] = hours.add(HOUR_FRAGMENTATION);
}
}
}
private void allocateRemindingHours(BigDecimal[] hoursToAllocatePerDay, BigDecimal remindingHours) {
final int length = hoursToAllocatePerDay.length;
// Add reminding hours to best fit in a way that the distribution of
// hours grows continuously
for (int i = 0; i < length - 1; i++) {
BigDecimal current = hoursToAllocatePerDay[i];
BigDecimal next = hoursToAllocatePerDay[i+1];
if (current.add(remindingHours).compareTo(next) <= 0) {
hoursToAllocatePerDay[i] = current.add(remindingHours);
return;
}
}
// Add reminding hours to last day
BigDecimal lastDay = hoursToAllocatePerDay[length - 1];
hoursToAllocatePerDay[length - 1] = lastDay.add(remindingHours);
}
private void allocate(ResourceAllocation<?> resourceAllocation,
LocalDate day, EffortDuration hours) {
final LocalDate nextDay = day.plusDays(1);
resourceAllocation.withPreviousAssociatedResources()
.onInterval(day, nextDay).allocate(hours);
}
private BigDecimal[] roundValues(BigDecimal[] allocatedHoursPerDay,
BigDecimal truncateValue) {
BigDecimal[] result = new BigDecimal[allocatedHoursPerDay.length];
BigDecimal reminder = BigDecimal.ZERO;
for (int i = 0; i < result.length; i++) {
BigDecimal value = allocatedHoursPerDay[i];
value = value.add(reminder);
BigDecimal intPart = intPart(value);
BigDecimal decimalPart = decimalPart(value);
reminder = calculateReminder(decimalPart, truncateValue);
decimalPart = decimalPart.subtract(reminder);
result[i] = intPart.add(decimalPart);
}
return result;
}
private BigDecimal calculateReminder(BigDecimal decimalPart,
BigDecimal truncateValue) {
BigDecimal[] result = decimalPart.divideAndRemainder(truncateValue);
return result[1];
}
private BigDecimal intPart(BigDecimal bd) {
return BigDecimal.valueOf(bd.intValue());
}
private BigDecimal decimalPart(BigDecimal bd) {
return bd.subtract(intPart(bd));
}
private BigDecimal sumHoursPerDay(BigDecimal[] hoursPerDay) {
BigDecimal result = BigDecimal.ZERO;
for (int i = 0; i < hoursPerDay.length; i++) {
result = result.add(hoursPerDay[i]);
}
return result;
}
private BigDecimal[] generateHoursToAllocateFor(int days, int hours) {
BigDecimal[] valuesPerDay = generatePointValuesForDays(days);
BigDecimal[] acummulatedHoursPerDay = calculateNumberOfAccumulatedHoursForDays(valuesPerDay, hours);
BigDecimal[] allocatedHoursPerDay = calculateNumberOfAllocatedHoursForDays(acummulatedHoursPerDay);
return allocatedHoursPerDay;
}
private BigDecimal[] generatePointValuesForDays(int days) {
final BigDecimal dayIntervalConstant = getDayIntervalConstant(days);
BigDecimal[] result = new BigDecimal[days];
for (int i = 0; i < days; i++) {
result[i] = BigDecimal.valueOf(-6)
.add(dayIntervalConstant.multiply(BigDecimal.valueOf(i)));
}
return result;
}
private BigDecimal[] calculateNumberOfAllocatedHoursForDays(BigDecimal[] acummulatedHoursPerDay) {
BigDecimal[] result = new BigDecimal[acummulatedHoursPerDay.length];
result[0] = acummulatedHoursPerDay[0];
for (int i = 1; i < result.length; i++) {
result[i] = acummulatedHoursPerDay[i].subtract(acummulatedHoursPerDay[i - 1]);
}
return result;
}
private BigDecimal[] calculateNumberOfAccumulatedHoursForDays(
BigDecimal[] dayValues, int totalHours) {
BigDecimal[] result = new BigDecimal[dayValues.length];
for (int i = 0; i < dayValues.length; i++) {
result[i] = calculateNumberOfAccumulatedHoursAtDay(dayValues[i], totalHours);
}
return result;
}
private BigDecimal calculateNumberOfAccumulatedHoursAtDay(
BigDecimal valueAtOneDay, int totalHours) {
BigDecimal epow = BigDecimal.valueOf(Math.pow(Math.E, valueAtOneDay
.negate().doubleValue()));
BigDecimal denominator = BigDecimal.valueOf(1).add(epow);
return BigDecimal.valueOf(totalHours).divide(denominator,
PRECISSION, ROUND_MODE);
}
// 12 divide by days
private BigDecimal getDayIntervalConstant(int days) {
return BigDecimal.valueOf(12).divide(BigDecimal.valueOf(days),
PRECISSION, ROUND_MODE);
}
@Override
public boolean isManual() {
return false;
}
}