/*
* 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-2012 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.workingday;
import static org.libreplan.business.workingday.EffortDuration.seconds;
import static org.libreplan.business.workingday.EffortDuration.zero;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import javax.validation.constraints.NotNull;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.libreplan.business.calendars.entities.Capacity;
/**
* <p>
* Instances of this class represent values so immutable objects are used. In
* order to do modifications new instances must be created.
* </p>
* <p>
* A date type that represents a point inside a working day. This doesn't
* translate directly to a concrete DateTime because the working day can start
* at an arbitrary hour.
* </p>
* <p>
* It represents the instant at which {@link #effortDuration} has been elapsed
* in the working day specified by the field {@link #date}. Since the amount of
* time for a working day is measured with an {@link EffortDuration}, to
* indicate the point inside a working day an {@link EffortDuration} is
* specified.
* </p>
* <p>
* For example, a IntraDayDate with a date such as 23/07/2010 and an effort
* duration of 2 hours represents the moment on 23/07/2010 at which 2 hours of
* the working day had been elapsed. Normally this object can't be converted to
* a precise {@link DateTime} due to not knowing the timetable of the workers.
* Nevertheless, this object is useful anyway in order to know how many effort
* is left at the working day.
* </p>
*
* @see PartialDay
* @see LocalDate
* @see EffortDuration
*
* @author Óscar González Fernández
* @author Manuel Rego Casasnovas <rego@igalia.com>
*
*/
public class IntraDayDate implements Comparable<IntraDayDate> {
public static IntraDayDate min(IntraDayDate... dates) {
Validate.noNullElements(dates);
return Collections.min(Arrays.asList(dates));
}
public static IntraDayDate max(IntraDayDate... dates) {
Validate.noNullElements(dates);
return Collections.max(Arrays.asList(dates));
}
public static IntraDayDate startOfDay(LocalDate date) {
return create(date, zero());
}
public static IntraDayDate create(LocalDate date, EffortDuration effortDuration) {
return new IntraDayDate(date, effortDuration);
}
private LocalDate date;
private EffortDuration effortDuration;
/**
* Default constructor for hibernate do not use!
*/
public IntraDayDate() {
}
private IntraDayDate(LocalDate date, EffortDuration effortDuration) {
Validate.notNull(date);
Validate.notNull(effortDuration);
this.date = date;
this.effortDuration = effortDuration;
}
@NotNull
public LocalDate getDate() {
return date;
}
/**
* <p>
* The duration elapsed projected to a day allocated with one resource per
* day.
* </p>
* <p>
* So, for example, if 8 hours are elapsed and the resources per day are 2,
* this value should be 4. If 4 hours are elapsed and the resources per day
* are 0.5, this value should be 8.
* <p>
* More generally, this value is the effort applied on {@link #date} divided
* by the resources per day being applied.
* </p>
* <p>
* This amount is useful in order to determine how much spare room for work
* is left on {@link #date}.
* </p>
*
* @return that duration
*/
public EffortDuration getEffortDuration() {
return effortDuration == null ? EffortDuration.zero() : effortDuration;
}
public boolean isStartOfDay() {
return getEffortDuration().isZero();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof IntraDayDate) {
IntraDayDate other = (IntraDayDate) obj;
return this.date.equals(other.date)
&& this.getEffortDuration().equals(
other.getEffortDuration());
}
return false;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(this.date)
.append(this.getEffortDuration()).toHashCode();
}
public boolean areSameDay(Date date) {
return areSameDay(LocalDate.fromDateFields(date));
}
public boolean areSameDay(LocalDate date) {
return this.date.equals(date);
}
public DateTime toDateTimeAtStartOfDay() {
return this.date.toDateTimeAtStartOfDay();
}
@Override
public int compareTo(IntraDayDate other) {
int result = date.compareTo(other.date);
if (result == 0) {
result = getEffortDuration().compareTo(other.getEffortDuration());
}
return result;
}
/**
* Return the day which is the exclusive end given this {@link IntraDayDate}
* @return
*/
public LocalDate asExclusiveEnd() {
if (isStartOfDay()) {
return getDate();
}
return getDate().plusDays(1);
}
public int compareTo(LocalDate other) {
int result = this.date.compareTo(other);
if (result != 0) {
return result;
}
return isStartOfDay() ? 0 : 1;
}
/**
* It's an interval of {@link IntraDayDate}. It must not elapse more than
* one day. It allows to represent a subinterval of the working day. It
* allows to know how much effort can be spent in this day.
*/
public static class PartialDay {
public static PartialDay wholeDay(LocalDate date) {
return new PartialDay(IntraDayDate.startOfDay(date),
IntraDayDate.startOfDay(date.plusDays(1)));
}
private final IntraDayDate start;
private final IntraDayDate end;
public PartialDay(IntraDayDate start, IntraDayDate end) {
Validate.notNull(start);
Validate.notNull(end);
Validate.isTrue(end.compareTo(start) >= 0);
Validate.isTrue(start.areSameDay(end.getDate())
|| (start.areSameDay(end.getDate().minusDays(1)) && end
.getEffortDuration().isZero()));
this.start = start;
this.end = end;
}
public IntraDayDate getStart() {
return start;
}
public IntraDayDate getEnd() {
return end;
}
public LocalDate getDate() {
return start.getDate();
}
/**
* <p>
* Limits the standard duration that can be worked in a day taking into
* account this day duration.
* </p>
* <ul>
* <li>
* If the day is whole then the duration returned is all.</li>
* <li>If the day has an end and a no zero start, the result will be:
* <code>max(duration, end) - start</code></li>
* <li>If the day has a no zero start that must be discounted from the
* duration</li>
* <li>If the day has an end, the duration must not surpass this end</li>
* </ul>
*
* @param standardWorkingDayDuration
* @return a duration that can be employed taking into consideration
* this day
*/
public EffortDuration limitWorkingDay(
EffortDuration standardWorkingDayDuration) {
if (isWholeDay()) {
return standardWorkingDayDuration;
}
EffortDuration alreadyElapsedInDay = start.getEffortDuration();
if (alreadyElapsedInDay.compareTo(standardWorkingDayDuration) >= 0) {
return zero();
}
EffortDuration durationLimitedByEnd = standardWorkingDayDuration;
if (!end.getEffortDuration().isZero()) {
durationLimitedByEnd = EffortDuration.min(
end.getEffortDuration(), standardWorkingDayDuration);
}
return durationLimitedByEnd.minus(alreadyElapsedInDay);
}
public Capacity limitCapacity(Capacity capacity) {
if (capacity.getAllowedExtraEffort() == null) {
EffortDuration effort = limitWorkingDay(capacity
.getStandardEffort());
return Capacity.create(effort).overAssignableWithoutLimit(
capacity.isOverAssignableWithoutLimit());
}
EffortDuration allEffort = capacity.getStandardEffort().plus(
capacity.getAllowedExtraEffort());
EffortDuration limited = limitWorkingDay(allEffort);
EffortDuration newStandard = EffortDuration.min(limited,
capacity.getStandardEffort());
return Capacity
.create(newStandard)
.withAllowedExtraEffort(
EffortDuration.min(limited.minus(newStandard)))
.overAssignableWithoutLimit(
capacity.isOverAssignableWithoutLimit());
}
private boolean isWholeDay() {
return start.getEffortDuration().isZero()
&& end.getEffortDuration().isZero();
}
@Override
public String toString() {
return Arrays.toString(new Object[] { start, end });
}
}
interface IterationPredicate {
public boolean hasNext(IntraDayDate current);
public IntraDayDate limitNext(IntraDayDate nextDay);
}
public static class UntilEnd implements IterationPredicate {
private final IntraDayDate endExclusive;
public UntilEnd(IntraDayDate endExclusive) {
this.endExclusive = endExclusive;
}
@Override
public final boolean hasNext(IntraDayDate current) {
return hasNext(current.compareTo(endExclusive) < 0);
}
protected boolean hasNext(boolean currentDateIsLessThanEnd) {
return currentDateIsLessThanEnd;
}
@Override
public IntraDayDate limitNext(IntraDayDate nextDay) {
return min(nextDay, endExclusive);
}
}
/**
* Returns an on demand {@link Iterable} that gives all the days from
* <code>this</code> to end
*
* @param endExclusive
* @return an on demand iterable
*/
public Iterable<PartialDay> daysUntil(final IntraDayDate endExclusive) {
Validate.isTrue(compareTo(endExclusive) <= 0);
return daysUntil(new UntilEnd(endExclusive));
}
public int numberOfDaysUntil(IntraDayDate end) {
Validate.isTrue(compareTo(end) <= 0);
Days daysBetween = Days.daysBetween(getDate(), end.getDate());
if (getEffortDuration().compareTo(end.getEffortDuration()) <= 0) {
return daysBetween.getDays();
} else {
return daysBetween.getDays() - 1;
}
}
public Iterable<PartialDay> daysUntil(final UntilEnd predicate) {
return new Iterable<IntraDayDate.PartialDay>() {
@Override
public Iterator<PartialDay> iterator() {
return createIterator(IntraDayDate.this, predicate);
}
};
}
public static List<PartialDay> toList(Iterable<PartialDay> days) {
List<PartialDay> result = new ArrayList<IntraDayDate.PartialDay>();
for (PartialDay each : days) {
result.add(each);
}
return result;
}
private static Iterator<PartialDay> createIterator(final IntraDayDate start, final IterationPredicate predicate) {
return new Iterator<IntraDayDate.PartialDay>() {
private IntraDayDate current = start;
@Override
public boolean hasNext() {
return predicate.hasNext(current);
}
@Override
public PartialDay next() {
if ( !hasNext() ) {
throw new NoSuchElementException();
}
IntraDayDate start = current;
current = calculateNext(current);
return new PartialDay(start, current);
}
private IntraDayDate calculateNext(IntraDayDate date) {
IntraDayDate nextDay = IntraDayDate.startOfDay(date.date.plusDays(1));
return predicate.limitNext(nextDay);
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public String toString() {
return Arrays.toString(new Object[] { date, effortDuration });
}
public IntraDayDate nextDayAtStart() {
return IntraDayDate.startOfDay(getDate().plusDays(1));
}
public IntraDayDate previousDayAtStart() {
return IntraDayDate.startOfDay(getDate().minusDays(1));
}
/**
* @return the next day or the same day if this {@link IntraDayDate} has no
* duration.
*/
public LocalDate roundUp() {
return asExclusiveEnd();
}
/**
* @return A date resulting of striping this {@link IntraDayDate} of its
* duration
*/
public LocalDate roundDown() {
return date;
}
/**
* Calculates a new {@link IntraDayDate} adding {@link EffortDuration
* effort} to {@link IntraDayDate this}. It considers the provided
* {@link ResourcesPerDay resourcesPerDay}, so if the resources per day is
* big the effort taken will be less. The date will stay the same, i.e. the
* returned {@link IntraDayDate} is on the same day.
*
* @param resourcesPerDay
* @param effort
* @return a new {@link IntraDayDate}
*/
public IntraDayDate increaseBy(ResourcesPerDay resourcesPerDay, EffortDuration effort) {
EffortDuration newEnd = this.getEffortDuration().plus(calculateProportionalDuration(resourcesPerDay, effort));
return IntraDayDate.create(getDate(), newEnd);
}
private EffortDuration calculateProportionalDuration(ResourcesPerDay resourcesPerDay, EffortDuration effort) {
int seconds = effort.getSeconds();
BigDecimal end = new BigDecimal(seconds).divide(resourcesPerDay.getAmount(), RoundingMode.HALF_UP);
return seconds(end.intValue());
}
/**
* The same as
* {@link IntraDayDate#increaseBy(ResourcesPerDay, EffortDuration)} but
* decreasing the effort. The date will stay the same, i.e. the returned
* {@link IntraDayDate} is on the same day.
*
* @see IntraDayDate#increaseBy(ResourcesPerDay, EffortDuration)
* @param resourcesPerDay
* @param effort
* @return a new {@link IntraDayDate}
*/
public IntraDayDate decreaseBy(ResourcesPerDay resourcesPerDay, EffortDuration effort) {
EffortDuration proportionalDuration = calculateProportionalDuration(resourcesPerDay, effort);
if ( getEffortDuration().compareTo(proportionalDuration) > 0 ) {
return IntraDayDate.create(getDate(), getEffortDuration().minus(proportionalDuration));
} else {
return IntraDayDate.startOfDay(getDate());
}
}
public static IntraDayDate convert(LocalDate date, IntraDayDate morePreciseAlternative) {
LocalDate morePreciseDate = morePreciseAlternative.getDate();
if ( morePreciseDate.equals(date) ) {
return morePreciseAlternative;
}
return startOfDay(date);
}
/**
* Returns the {@link EffortDuration} until {@code end} considering 8h per
* day of effort.
*
* @param end
* @return The {@link EffortDuration} until {@code end}
*/
public EffortDuration effortUntil(IntraDayDate end) {
Validate.isTrue(compareTo(end) <= 0);
int days = Days.daysBetween(getDate(), end.getDate()).getDays();
EffortDuration result = EffortDuration.hours(days * 8);
if ( !getEffortDuration().isZero()) {
result = result.minus(EffortDuration.hours(8));
result = result.plus(EffortDuration.hours(8).minus(getEffortDuration()));
}
if ( !end.getEffortDuration().isZero() ) {
result = result.plus(end.getEffortDuration());
}
return result;
}
/**
* Returns the {@link IntraDayDate} adding the {@code effort} considering 8h
* per day of effort.
*
* @param effort
* @return The {@link IntraDayDate} result of adding the {@code effort}
*/
public IntraDayDate addEffort(EffortDuration effort) {
int secondsPerDay = 3600 * 8;
int seconds = effort.getSeconds();
if (!getEffortDuration().isZero()) {
seconds += getEffortDuration().getSeconds();
}
int days = seconds / secondsPerDay;
EffortDuration extraEffort = EffortDuration.zero();
int extra = seconds % secondsPerDay;
if (extra != 0) {
extraEffort = extraEffort.plus(EffortDuration.seconds(extra));
}
return IntraDayDate.create(getDate().plusDays(days), extraEffort);
}
}