diff --git a/navalplanner-business/src/main/java/org/navalplanner/business/common/ProportionalDistributor.java b/navalplanner-business/src/main/java/org/navalplanner/business/common/ProportionalDistributor.java new file mode 100644 index 000000000..fce5eabb3 --- /dev/null +++ b/navalplanner-business/src/main/java/org/navalplanner/business/common/ProportionalDistributor.java @@ -0,0 +1,132 @@ +/* + * This file is part of ###PROJECT_NAME### + * + * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e + * Desenvolvemento Tecnolóxico de Galicia + * + * 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.common; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Óscar González Fernández + */ +public class ProportionalDistributor { + + public static ProportionalDistributor create(int... initialShares) { + return new ProportionalDistributor(toProportions( + sumIntegerParts(initialShares), initialShares)); + } + + private static int sumIntegerParts(int[] numbers) { + int sum = 0; + for (Number each : numbers) { + sum += each.intValue(); + } + return sum; + } + + private static BigDecimal[] toProportions(int initialTotal, int... shares) { + BigDecimal total = new BigDecimal(initialTotal); + BigDecimal[] result = new BigDecimal[shares.length]; + for (int i = 0; i < result.length; i++) { + result[i] = new BigDecimal(shares[i]).divide(total, 4, + RoundingMode.DOWN); + } + return result; + } + + private static class ProportionWithPosition implements + Comparable { + + public static List transform( + BigDecimal[] proportions) { + List result = new ArrayList(); + for (int i = 0; i < proportions.length; i++) { + result.add(new ProportionWithPosition(i, proportions[i])); + } + return result; + } + + final int position; + final BigDecimal proportion; + + ProportionWithPosition(int position, BigDecimal proportion) { + this.position = position; + this.proportion = proportion; + } + + @Override + public int compareTo(ProportionWithPosition other) { + return proportion.compareTo(other.proportion); + } + + } + + private final BigDecimal[] proportions; + + private ProportionalDistributor(BigDecimal[] proportions) { + this.proportions = proportions; + } + + public int[] distribute(final int total) { + int[] result = new int[proportions.length]; + int remaining = total - assignIntegerParts(total, result); + if (remaining == 0) { + return result; + } + BigDecimal[] currentProportions = toProportions(total, result); + assignRemaining(result, currentProportions, remaining); + return result; + } + + private int assignIntegerParts(int current, int[] result) { + int substract = 0; + for (int i = 0; i < proportions.length; i++) { + int intValue = proportions[i].multiply(new BigDecimal(current)) + .intValue(); + if (intValue > 0) { + result[i] = result[i] + intValue; + substract += intValue; + } + } + return substract; + } + + private void assignRemaining(int[] result, BigDecimal[] currentProportions, + int remaining) { + List transform = ProportionWithPosition + .transform(difference(currentProportions)); + Collections.sort(transform, Collections.reverseOrder()); + for (int i = 0; i < remaining; i++) { + ProportionWithPosition proportionWithPosition = transform.get(i); + result[proportionWithPosition.position] = result[proportionWithPosition.position] + 1; + } + } + + private BigDecimal[] difference(BigDecimal[] pr) { + BigDecimal[] result = new BigDecimal[proportions.length]; + for (int i = 0; i < result.length; i++) { + result[i] = proportions[i].subtract(pr[i]); + } + return result; + } + +} diff --git a/navalplanner-business/src/test/java/org/navalplanner/business/common/ProportionalDistributorTest.java b/navalplanner-business/src/test/java/org/navalplanner/business/common/ProportionalDistributorTest.java new file mode 100644 index 000000000..f8be386fe --- /dev/null +++ b/navalplanner-business/src/test/java/org/navalplanner/business/common/ProportionalDistributorTest.java @@ -0,0 +1,129 @@ +/* + * This file is part of ###PROJECT_NAME### + * + * Copyright (C) 2009 Fundación para o Fomento da Calidade Industrial e + * Desenvolvemento Tecnolóxico de Galicia + * + * 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.common; + +import static org.junit.Assert.assertThat; + +import java.util.Arrays; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Test; + +/** + * @author Óscar González Fernández + * + */ +public class ProportionalDistributorTest { + + @Test + public void mustGiveTheSameDistributionForSameTotal() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 100, 200); + assertThat(distributor.distribute(300), equalToDistribution(100, 200)); + } + + @Test + public void exactDivisionsWorkOk() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 100, 100, 100); + assertThat(distributor.distribute(600), equalToDistribution(200, 200, + 200)); + } + + @Test + public void distributingZeroGivesZeroShares() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 100, 100, 100); + assertThat(distributor.distribute(0), equalToDistribution(0, 0, 0)); + } + + @Test + public void ifOneOfTheProportionsIsZeroAlwaysGivesZeros() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 100, 100, 0); + assertThat(distributor.distribute(100), equalToDistribution(50, 50, 0)); + } + + @Test + public void disputedPartGoesToFirstIfEqualWeight() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 10, 10, 10); + assertThat(distributor.distribute(10), equalToDistribution(4, 3, 3)); + } + + @Test + public void distributionIsKept() { + ProportionalDistributor distributor = ProportionalDistributor.create(2, + 3, 5); + assertThat(distributor.distribute(1), equalToDistribution(0, 0, 1)); + assertThat(distributor.distribute(2), equalToDistribution(0, 1, 1)); + assertThat(distributor.distribute(3), equalToDistribution(1, 1, 1)); + assertThat(distributor.distribute(4), equalToDistribution(1, 1, 2)); + assertThat(distributor.distribute(5), equalToDistribution(1, 2, 2)); + assertThat(distributor.distribute(6), equalToDistribution(1, 2, 3)); + assertThat(distributor.distribute(10), equalToDistribution(2, 3, 5)); + assertThat(distributor.distribute(7), equalToDistribution(1, 2, 4)); + } + + @Test + public void addingOneEachTime() { + ProportionalDistributor distributor = ProportionalDistributor.create( + 99, 101, 800); + assertThat(distributor.distribute(1), equalToDistribution(0, 0, 1)); + assertThat(distributor.distribute(3), equalToDistribution(0, 0, 3)); + assertThat(distributor.distribute(6), equalToDistribution(0, 1, 5)); + assertThat(distributor.distribute(7), equalToDistribution(1, 1, 5)); + assertThat(distributor.distribute(8), equalToDistribution(1, 1, 6)); + assertThat(distributor.distribute(9), equalToDistribution(1, 1, 7)); + assertThat(distributor.distribute(10), equalToDistribution(1, 1, 8)); + assertThat(distributor.distribute(11), equalToDistribution(1, 1, 9)); + assertThat(distributor.distribute(12), equalToDistribution(1, 1, 10)); + assertThat(distributor.distribute(13), equalToDistribution(1, 1, 11)); + assertThat(distributor.distribute(14), equalToDistribution(1, 2, 11)); + assertThat(distributor.distribute(15), equalToDistribution(1, 2, 12)); + assertThat(distributor.distribute(16), equalToDistribution(1, 2, 13)); + assertThat(distributor.distribute(17), equalToDistribution(2, 2, 13)); + assertThat(distributor.distribute(20), equalToDistribution(2, 2, 16)); + } + + private static Matcher equalToDistribution(final int... distribution) { + return new BaseMatcher() { + + @Override + public boolean matches(Object object) { + if (object instanceof int[]) { + int[] arg = (int[]) object; + return Arrays.equals(arg, distribution); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("must equal " + + Arrays.toString(distribution)); + } + }; + } + + +}