Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save creativearmenia/972de4674f9fcd1c98cad1dd91caafc0 to your computer and use it in GitHub Desktop.
Save creativearmenia/972de4674f9fcd1c98cad1dd91caafc0 to your computer and use it in GitHub Desktop.
Calculates correctly distributed percentages (that sums to 100%) of arrays, collections and maps of numbers.
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
import static java.util.stream.Collectors.toUnmodifiableMap;
import java.util.stream.IntStream;
public class Percentage {
public static List<BigDecimal> of(int decimalPlaces, double... parts) {
double total = Arrays.stream(parts).sum();
if (total == 0.0)
return Collections.unmodifiableList(Collections.nCopies(parts.length, BigDecimal.ZERO));
else
return adjustedPercentages(decimalPlaces, Arrays.stream(parts)
.map(part -> (part / total) * 100.0)
.toArray());
}
public static List<BigDecimal> of(int decimalPlaces, int... parts) {
return of(decimalPlaces, Arrays.stream(parts)
.asDoubleStream()
.toArray());
}
public static List<BigDecimal> of(int decimalPlaces, long... parts) {
return of(decimalPlaces, Arrays.stream(parts)
.asDoubleStream()
.toArray());
}
public static <T extends Number> List<BigDecimal> of(int decimalPlaces, List<T> parts) {
return of(decimalPlaces, parts.stream()
.mapToDouble(Number::doubleValue)
.toArray());
}
public static <K, V extends Number> Map<K, BigDecimal> of(int decimalPlaces, Map<K, V> parts) {
List<Entry<K, V>> entries = new ArrayList<>(parts.entrySet());
List<BigDecimal> percentages = of(decimalPlaces, entries.stream()
.map(Entry::getValue)
.mapToDouble(Number::doubleValue)
.toArray());
return IntStream
.range(0, entries.size())
.boxed()
.collect(toUnmodifiableMap(i -> entries.get(i).getKey(), percentages::get));
}
public static <K, V extends Collection<?>> Map<K, BigDecimal> ofSizes(int decimalPlaces, Map<K, V> parts) {
return of(decimalPlaces, parts.entrySet().stream()
.collect(toMap(Entry::getKey, e -> e.getValue().size())));
}
// Adjust de percentages using the Largest Remainder Method
// https://revs.runtime-revolution.com/getting-100-with-rounded-percentages-273ffa70252b
private static List<BigDecimal> adjustedPercentages(int decimalPlaces, double[] percentages) {
if (decimalPlaces < 0)
throw new IllegalArgumentException("Decimal places must be non-negative");
// Precision multiplier
long multiplier = (long) Math.pow(10, decimalPlaces);
long[] longPercentages = Arrays.stream(percentages)
.mapToLong(percentage -> (long) (percentage * multiplier))
.toArray();
// Difference to 100%
long difference = (100 * multiplier) - Arrays.stream(longPercentages).sum();
if (difference > 0) {
double[] remainders = Arrays.stream(percentages)
.map(percentage -> (percentage * multiplier) % 1.0)
.toArray();
// Distribute the difference in decreasing order of remainder
IntStream
.range(0, remainders.length)
.boxed()
.sorted((i, j) -> -1 * Double.compare(remainders[i], remainders[j]))
.limit(difference)
.forEachOrdered(i -> longPercentages[i]++);
}
assert Arrays.stream(longPercentages).sum() == (100 * multiplier);
return Arrays.stream(longPercentages)
.mapToObj(percentage -> BigDecimal.valueOf(percentage, decimalPlaces))
.collect(toUnmodifiableList());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment