org.libreplan.business.workingday.EffortDuration.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.business.workingday.EffortDuration.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.workingday;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.math.Fraction;

/**
 * <p>
 *     It represents some amount of effort.
 *     It's composed by some hours, minutes and seconds.
 *     Less granularity than a second can't be specified.
 * </p>
 * <p>
 *     This object can represent the predicted amount of work that a task takes,
 *     the scheduled amount of work for a working day, the amount of effort that a worker can work in a given day, etc.
 * </p>
 *
 * @author scar Gonzlez Fernndez <ogonzalez@igalia.com>
 */
public class EffortDuration implements Comparable<EffortDuration> {

    private static final Pattern lenientEffortDurationSpecification = Pattern.compile("(\\d+)(\\s*:\\s*\\d+\\s*)*");

    private static final Pattern contiguousDigitsPattern = Pattern.compile("\\d+");

    private final int seconds;

    private EffortDuration(int seconds) {
        Validate.isTrue(seconds >= 0, "seconds cannot be negative");
        this.seconds = seconds;
    }

    public enum Granularity {
        HOURS(3600), MINUTES(60), SECONDS(1);

        private final int secondsPerUnit;

        Granularity(int secondsPerUnit) {
            this.secondsPerUnit = secondsPerUnit;
        }

        static Granularity[] fromMoreCoarseToLessCoarse() {
            return Granularity.values();
        }

        public int toSeconds(int amount) {
            return secondsPerUnit * amount;
        }

        public int convertFromSeconds(int seconds) {
            return seconds / secondsPerUnit;
        }
    }

    /**
     * If an {@link EffortDuration} can't be parsed <code>null</code> is returned.
     * The hours field at least is required, the next fields are the minutes and seconds.
     * If there is more than one field, they are separated by colons.
     *
     * @param string
     * @return {@link EffortDuration}
     */
    public static EffortDuration parseFromFormattedString(String string) {
        Matcher matcher = lenientEffortDurationSpecification.matcher(string);
        if (matcher.find()) {
            List<String> parts = scan(contiguousDigitsPattern, string);
            assert parts.size() >= 1;

            return EffortDuration.hours(retrieveNumber(0, parts)).and(retrieveNumber(1, parts), Granularity.MINUTES)
                    .and(retrieveNumber(2, parts), Granularity.SECONDS);
        }
        return null;
    }

    private static List<String> scan(Pattern pattern, String text) {
        List<String> result = new ArrayList<>();
        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            result.add(matcher.group());
        }
        return result;
    }

    private static int retrieveNumber(int i, List<String> parts) {
        return i >= parts.size() ? 0 : Integer.parseInt(parts.get(i));
    }

    public interface IEffortFrom<T> {

        EffortDuration from(T each);
    }

    public static <T> EffortDuration sum(Iterable<? extends T> collection, IEffortFrom<T> effortFrom) {
        EffortDuration result = zero();
        for (T each : collection) {
            result = result.plus(effortFrom.from(each));
        }
        return result;
    }

    public static EffortDuration sum(EffortDuration... summands) {
        return sum(Arrays.asList(summands), new IEffortFrom<EffortDuration>() {
            @Override
            public EffortDuration from(EffortDuration each) {
                return each;
            }
        });
    }

    public static EffortDuration zero() {
        return elapsing(0, Granularity.SECONDS);
    }

    public static EffortDuration elapsing(int amount, Granularity granularity) {
        return new EffortDuration(granularity.toSeconds(amount));
    }

    public static EffortDuration hours(int amount) {
        return elapsing(amount, Granularity.HOURS);
    }

    public static EffortDuration minutes(int amount) {
        return elapsing(amount, Granularity.MINUTES);
    }

    public static EffortDuration seconds(int amount) {
        return elapsing(amount, Granularity.SECONDS);
    }

    public static EffortDuration fromHoursAsBigDecimal(BigDecimal hours) {
        BigDecimal secondsPerHour = new BigDecimal(3600);

        return elapsing(hours.multiply(secondsPerHour).intValue(), Granularity.SECONDS);
    }

    public int getHours() {
        return convertTo(Granularity.HOURS);
    }

    public int getMinutes() {
        return convertTo(Granularity.MINUTES);
    }

    public int getSeconds() {
        return convertTo(Granularity.SECONDS);
    }

    public int convertTo(Granularity granularity) {
        return granularity.convertFromSeconds(seconds);
    }

    public EffortDuration and(int amount, Granularity granularity) {
        return new EffortDuration(seconds + granularity.toSeconds(amount));
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof EffortDuration) {
            EffortDuration other = (EffortDuration) obj;

            return getSeconds() == other.getSeconds();
        }
        return false;
    }

    @Override
    public int hashCode() {
        return getSeconds();
    }

    public EnumMap<Granularity, Integer> decompose() {
        EnumMap<Granularity, Integer> result = new EnumMap<>(Granularity.class);
        int remainder = seconds;
        for (Granularity each : Granularity.fromMoreCoarseToLessCoarse()) {
            int value = each.convertFromSeconds(remainder);
            remainder -= value * each.toSeconds(1);
            result.put(each, value);
        }
        assert remainder == 0;

        return result;
    }

    @Override
    public int compareTo(EffortDuration other) {
        Validate.notNull(other);

        return seconds - other.seconds;
    }

    /**
     * Multiplies this duration by a scalar.
     *
     * <br />
     * <b>Warning:<b /> This method can cause an integer overflow and the result would be incorrect.
     * @param n
     * @return a duration that is the multiply of n and <code>this</code>
     */
    public EffortDuration multiplyBy(int n) {
        return EffortDuration.seconds(this.seconds * n);
    }

    /**
     * Divides this duration by a scalar.
     *
     * @param n
     *            a number greater than zero
     * @return a new duration that is the result of dividing <code>this</code>
     *         by n
     */
    public EffortDuration divideBy(int n) {
        Validate.isTrue(n > 0);
        return new EffortDuration(seconds / n);
    }

    /**
     * <p>
     *     Divides this duration by other returning the quotient.
     * </p>
     *
     * There can be a remainder left.
     *
     * @see #remainderFor(EffortDuration)
     * @param other
     * @return
     */
    public int divideBy(EffortDuration other) {
        return seconds / other.seconds;
    }

    public Fraction divivedBy(EffortDuration effortAssigned) {
        return Fraction.getFraction(this.seconds, effortAssigned.seconds);
    }

    /**
     * <p>
     *     Divides this duration by other (using total seconds) returning the quotient as BigDecimal.
     * </p>
     *
     * @param other
     * @return
     */
    public BigDecimal dividedByAndResultAsBigDecimal(EffortDuration other) {
        return other.isZero() ? BigDecimal.ZERO
                : new BigDecimal(this.getSeconds()).divide(new BigDecimal(other.getSeconds()), 8,
                        BigDecimal.ROUND_HALF_EVEN);
    }

    /**
     * Calculates the remainder resulting of doing the integer division of both durations.
     *
     * @see #divideBy(EffortDuration)
     * @param other
     * @return the remainder
     */
    public EffortDuration remainderFor(EffortDuration other) {
        int dividend = divideBy(other);

        return this.minus(other.multiplyBy(dividend));
    }

    /**
     * Pluses two {@link EffortDuration}.
     * <br />
     * <b>Warning:<b /> This method can cause an integer overflow and the result would be incorrect.
     *
     * @param other
     * @return a duration that is the sum of <code>this</code>
     *         {@link EffortDuration} and the other duration
     */
    public EffortDuration plus(EffortDuration other) {
        return new EffortDuration(seconds + other.seconds);
    }

    public boolean isZero() {
        return seconds == 0;
    }

    /**
     * Substracts two {@link EffortDuration}.
     * Because {@link EffortDuration durations} cannot be negative <code>this</code> must be bigger than the
     * parameter or the same.
     *
     * @param duration
     * @return the result of substracting the two durations
     * @throws IllegalArgumentException
     *             if the parameter is bigger than <code>this</code>
     */
    public EffortDuration minus(EffortDuration duration) {
        Validate.isTrue(this.compareTo(duration) >= 0, "minued must not be smaller than subtrahend");

        return new EffortDuration(seconds - duration.seconds);
    }

    public BigDecimal toHoursAsDecimalWithScale(int scale) {
        BigDecimal result = BigDecimal.ZERO;
        final BigDecimal secondsPerHour = new BigDecimal(3600);

        for (Entry<Granularity, Integer> each : decompose().entrySet()) {
            BigDecimal seconds = new BigDecimal(each.getKey().toSeconds(each.getValue()));
            result = result.add(seconds.divide(secondsPerHour, scale, BigDecimal.ROUND_HALF_UP));
        }
        return result;
    }

    /**
     * <p>
     *     Converts this duration in a number of hours.
     *     Uses a typical half up round, so for example one hour and half is converted to two hours.
     *     There is an exception though, when the duration is less than one hour and is not zero it's returned one.
     *     This is handy for avoiding infinite loops in some algorithms;
     *     when all code is converted to use {@link EffortDuration Effort Durations} this will no longer be necessary.
     * </p>
     *
     * So there are three cases:
     * <ul>
     *     <li>the duration is zero, 0 is returned</li>
     *     <li>if duration > 0 and duration < 1, 1 is returned</li>
     *     <li>if duration >= 1, typical half up round is done.</li>
     * </ul>
     * For example 1 hour and 20 minutes returns 1 hour, 1 hour and 30 minutes 2 hours.
     *
     * @return an integer number of hours
     */
    public int roundToHours() {
        return this.isZero() ? 0 : Math.max(1, roundHalfUpToHours(this.decompose()));
    }

    public static EffortDuration min(EffortDuration... durations) {
        return Collections.min(Arrays.asList(durations));
    }

    public static EffortDuration max(EffortDuration... durations) {
        return Collections.max(Arrays.asList(durations));
    }

    public static EffortDuration average(EffortDuration total, int items) {
        return EffortDuration.seconds(total.seconds / items);
    }

    private static int roundHalfUpToHours(EnumMap<Granularity, Integer> components) {
        int seconds = components.get(Granularity.SECONDS);
        int minutes = components.get(Granularity.MINUTES) + (seconds < 30 ? 0 : 1);
        int hours = components.get(Granularity.HOURS) + (minutes < 30 ? 0 : 1);

        return hours;
    }

    public String toString() {
        EnumMap<Granularity, Integer> valuesForEachUnit = decompose();
        Integer hours = valuesForEachUnit.get(Granularity.HOURS);
        Integer minutes = valuesForEachUnit.get(Granularity.MINUTES);
        Integer seconds = valuesForEachUnit.get(Granularity.SECONDS);

        return String.format("%d:%02d:%02d", hours, minutes, seconds);
    }

    public String toFormattedString() {
        EnumMap<Granularity, Integer> byGranularity = this.atNearestMinute().decompose();
        int hours = byGranularity.get(Granularity.HOURS);
        int minutes = byGranularity.get(Granularity.MINUTES);

        return minutes == 0 ? String.format("%d", hours) : String.format("%d:%02d", hours, minutes);
    }

    public EffortDuration atNearestMinute() {
        EnumMap<Granularity, Integer> decompose = this.decompose();
        int seconds = decompose.get(Granularity.SECONDS);

        return seconds >= 30 ? this.plus(EffortDuration.seconds(60 - seconds))
                : this.minus(EffortDuration.seconds(seconds));
    }

}