From 5f5275f62f39fb9778488d900ad5f0eda898fec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20Gonz=C3=A1lez=20Fern=C3=A1ndez?= Date: Thu, 11 Aug 2011 18:19:50 +0200 Subject: [PATCH] Add class to distribute an EffortDuration considering the capacities FEA: ItEr75S04BugFixing --- .../allocationalgorithms/Distributor.java | 207 ++++++++++++++++++ .../allocationalgorithms/DistributorTest.java | 105 +++++++++ 2 files changed, 312 insertions(+) create mode 100644 navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/Distributor.java create mode 100644 navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/allocationalgorithms/DistributorTest.java diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/Distributor.java b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/Distributor.java new file mode 100644 index 000000000..2e9d99309 --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/planner/entities/allocationalgorithms/Distributor.java @@ -0,0 +1,207 @@ +/* + * This file is part of NavalPlan + * + * 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 . + */ +package org.navalplanner.business.planner.entities.allocationalgorithms; + +import static org.navalplanner.business.workingday.EffortDuration.seconds; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.lang.Validate; +import org.navalplanner.business.calendars.entities.Capacity; +import org.navalplanner.business.planner.entities.Share; +import org.navalplanner.business.planner.entities.ShareDivision; +import org.navalplanner.business.workingday.EffortDuration; +import org.navalplanner.business.workingday.EffortDuration.IEffortFrom; + +/** + * Distributes an EffortDuration among several capacities. It respects the extra + * hours requirements of the {@link Capacity capacities} and distributes the + * effort evenly. + * + * @author Óscar González Fernández + */ +public class Distributor { + + public static Distributor among(Capacity... capacities) { + return among(Arrays.asList(capacities)); + } + + public static Distributor among(Collection capacities) { + Validate.noNullElements(capacities); + + return new Distributor(capacities.toArray(new Capacity[0])); + } + + private final Capacity[] capacities; + + private final List normalCapacityShares; + + private final List limitedOverloadShares; + + private final List unlimitedOverloadShares; + + private final List> phases = new ArrayList>(); + + private Distributor(Capacity[] capacities) { + Validate.notNull(capacities); + this.capacities = capacities; + this.normalCapacityShares = createNormalCapacityShares(capacities); + this.limitedOverloadShares = createOverloadShares(capacities); + this.unlimitedOverloadShares = createUnlimitedShares(capacities); + this.phases.add(normalCapacityShares); + this.phases.add(limitedOverloadShares); + this.phases.add(this.unlimitedOverloadShares); + } + + private static List createNormalCapacityShares(Capacity[] capacities) { + List result = new ArrayList(); + for (Capacity each : capacities) { + result.add(createNormalCapacityShare(each)); + } + return result; + } + + private List createOverloadShares(Capacity[] capacities) { + List result = new ArrayList(); + EffortDuration maxExtraEffort = getMaxExtraEffort(capacities); + for (Capacity each : capacities) { + result.add(maxExtraEffort == null ? noSpaceAvailable() + : createOverloadShare(each, maxExtraEffort)); + } + return result; + } + + private EffortDuration getMaxExtraEffort(Capacity[] capacities) { + if (capacities.length == 0) { + return null; + } + Capacity max = Collections.max(Arrays.asList(capacities), new Comparator(){ + + @Override + public int compare(Capacity o1, Capacity o2) { + if (o1.getAllowedExtraEffort() == o2.getAllowedExtraEffort()) { + return 0; + } else if (o1.getAllowedExtraEffort() == null) { + return -1; + } else if (o2.getAllowedExtraEffort() == null) { + return 1; + } + return o1.getAllowedExtraEffort().compareTo( + o2.getAllowedExtraEffort()); + } + }); + return max.getAllowedExtraEffort(); + + } + + private static Share createNormalCapacityShare(Capacity each) { + return new Share(-each.getStandardEffort().getSeconds()); + } + + private Share createOverloadShare(Capacity each, + EffortDuration maxExtraEffort) { + if (each.getAllowedExtraEffort() == null && !each.isOverAssignableWithoutLimit()) { + return noSpaceAvailable(); + } + EffortDuration effort = each.getAllowedExtraEffort() != null ? each + .getAllowedExtraEffort() : maxExtraEffort; + return new Share(-effort.getSeconds()); + } + + private Share noSpaceAvailable() { + return new Share(Integer.MAX_VALUE); + } + + private List createUnlimitedShares(Capacity[] capacities) { + List result = new ArrayList(); + for (Capacity each : capacities) { + result.add(each.isOverAssignableWithoutLimit() ? new Share(0) + : noSpaceAvailable()); + } + return result; + } + + public List distribute(EffortDuration effort) { + EffortDuration[] result = new EffortDuration[capacities.length]; + Arrays.fill(result, EffortDuration.zero()); + + for (List shares : phases) { + EffortDuration remaining = effort.minus(sum(result)); + if (remaining.isZero()) { + return asList(result); + } + result = limitByCapacities(sum(result, + distribute(remaining, shares))); + } + return asList(result); + } + + private EffortDuration[] sum(EffortDuration[] a, EffortDuration[] b) { + EffortDuration[] result = new EffortDuration[a.length]; + for (int i = 0; i < result.length; i++) { + result[i] = a[i].plus(b[i]); + } + return result; + } + + private EffortDuration[] distribute(EffortDuration effort, + List shares) { + ShareDivision division = ShareDivision.create(shares); + return fromSecondsToDurations(division.to(division.plus(effort + .getSeconds()))); + } + + private List asList(EffortDuration[] acc) { + return new ArrayList(Arrays.asList(acc)); + } + + private EffortDuration[] limitByCapacities(EffortDuration[] efforts) { + EffortDuration[] result = new EffortDuration[efforts.length]; + for (int i = 0; i < efforts.length; i++) { + result[i] = capacities[i].limitDuration(efforts[i]); + } + return result; + } + + private EffortDuration[] fromSecondsToDurations(int[] seconds) { + EffortDuration[] result = new EffortDuration[seconds.length]; + for (int i = 0; i < result.length; i++) { + result[i] = seconds(seconds[i]); + } + return result; + } + + private EffortDuration sum(EffortDuration[] durations) { + return EffortDuration.sum(asList(durations), + new IEffortFrom() { + + @Override + public EffortDuration from(EffortDuration each) { + return each; + }}); + } + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/allocationalgorithms/DistributorTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/allocationalgorithms/DistributorTest.java new file mode 100644 index 000000000..4b14ddfeb --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/test/planner/entities/allocationalgorithms/DistributorTest.java @@ -0,0 +1,105 @@ +package org.navalplanner.business.test.planner.entities.allocationalgorithms; + +import static org.junit.Assert.assertThat; +import static org.navalplanner.business.workingday.EffortDuration.hours; + +import java.util.Arrays; +import java.util.List; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.navalplanner.business.calendars.entities.Capacity; +import org.navalplanner.business.planner.entities.allocationalgorithms.Distributor; +import org.navalplanner.business.workingday.EffortDuration; + +public class DistributorTest { + + @Test + public void theEffortIsDistributedEvenly() { + Distributor distributor = Distributor.among(Capacity.create(hours(8)), + Capacity.create(hours(8))); + + assertThat(distributor.distribute(hours(16)), + hasEfforts(hours(8), hours(8))); + assertThat(distributor.distribute(hours(8)), + hasEfforts(hours(4), hours(4))); + } + + @Test + public void ifNoOverassignationAllowedNotAllIsDistributed() { + Distributor distributor = Distributor.among(Capacity.create(hours(8)) + .notOverAssignableWithoutLimit(), Capacity.create(hours(8)) + .notOverAssignableWithoutLimit()); + + assertThat(distributor.distribute(hours(18)), + hasEfforts(hours(8), hours(8))); + } + + @Test + public void theOverAssignableCapacityGetsTheRest() { + Distributor distributor = Distributor.among(Capacity.create(hours(8)) + .notOverAssignableWithoutLimit(), Capacity.create(hours(8)) + .overAssignableWithoutLimit()); + + assertThat(distributor.distribute(hours(14)), + hasEfforts(hours(7), hours(7))); + assertThat(distributor.distribute(hours(16)), + hasEfforts(hours(8), hours(8))); + + assertThat(distributor.distribute(hours(18)), + hasEfforts(hours(8), hours(10))); + } + + @Test + public void mixingNotOverAssignableAndOverassignableToALimit() { + Distributor distributor = Distributor.among(Capacity.create(hours(8)) + .withAllowedExtraEffort(hours(2)), Capacity.create(hours(8)) + .notOverAssignableWithoutLimit()); + + assertThat(distributor.distribute(hours(16)), + hasEfforts(hours(8), hours(8))); + assertThat(distributor.distribute(hours(17)), + hasEfforts(hours(9), hours(8))); + assertThat(distributor.distribute(hours(18)), + hasEfforts(hours(10), hours(8))); + assertThat(distributor.distribute(hours(19)), + hasEfforts(hours(10), hours(8))); + } + + @Test + public void ifNoCapacityItReturnsZeroHours() { + Distributor distributor = Distributor.among(Capacity.create(hours(0)) + .notOverAssignableWithoutLimit()); + assertThat(distributor.distribute(hours(4)), hasEfforts(hours(0))); + } + + private Matcher> hasEfforts( + final EffortDuration... efforts) { + return new BaseMatcher>() { + + @Override + public boolean matches(Object arg) { + return Arrays.equals(efforts, toArray(arg)); + } + + private EffortDuration[] toArray(Object value) { + if (value instanceof EffortDuration[]) { + return (EffortDuration[]) value; + } + if (value instanceof List) { + List list = (List) value; + return list.toArray(new EffortDuration[0]); + } + return null; + } + + @Override + public void describeTo(Description description) { + description.appendText(Arrays.toString(efforts)); + } + }; + } + +}