Skip to content

Instantly share code, notes, and snippets.

@chrismay
Last active October 22, 2017 19:25
Show Gist options
  • Save chrismay/82ad875b508d9aabea6c0a078e6135b0 to your computer and use it in GitHub Desktop.
Save chrismay/82ad875b508d9aabea6c0a078e6135b0 to your computer and use it in GitHub Desktop.
An implementation of Hamilton's method for Smallest-Remainder rounding of percentages.
package chris.impl;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.stream.Collectors.toList;
import static chris.impl.Hamilton.IndexWrapper.zipWithIndex;
public class Hamilton {
public static <T> List<T> normalisePercentages(
List<T> inputs,
Function<T, Double> getNumber,
Function<Double, Function<T, T>> newValue) {
double total = inputs
.stream()
.map(getNumber)
.mapToDouble(Double::doubleValue)
.sum();
double multiple = 100 / total;
Function<T, T> normalise = t -> newValue.apply(multiple * getNumber.apply(t)).apply(t);
return inputs.stream().map(normalise).collect(toList());
}
public static <T> List<T> normalisePercentagesWithRounding(
List<T> unNormalisedInputs,
Function<T, Double> getNumber,
Function<Double, Function<T, T>> newValue) {
final Function<T, T> roundUp = t -> getNumber.andThen(Math::ceil).andThen(newValue).apply(t).apply(t);
final Function<T, T> roundDown = t -> getNumber.andThen(Math::floor).andThen(newValue).apply(t).apply(t);
final Comparator<IndexWrapper<T>> byRemainderAsc = IndexWrapper.compareContentsWith(t -> getNumber.apply(t) % 1);
final List<T> inputs = normalisePercentages(unNormalisedInputs, getNumber, newValue);
final int difference = 100 - inputs.stream()
.map(roundDown)
.map(getNumber)
.mapToInt(Double::intValue)
.sum();
final List<IndexWrapper<T>> sortables = zipWithIndex(inputs.stream())
.sorted(byRemainderAsc.reversed())
.collect(toList());
final List<IndexWrapper<T>> toCeiling = sortables.subList(0, difference);
final List<IndexWrapper<T>> toFloor = sortables.subList(difference, sortables.size());
return Stream.concat(
toCeiling.stream().map(s -> s.map(roundUp)),
toFloor.stream().map(s -> s.map(roundDown)))
.sorted()
.map(IndexWrapper::getValue)
.collect(toList());
}
public static void main(String[] args) {
Function<HasNumeric, Double> value = s -> s.num;
Function<Double, Function<HasNumeric, HasNumeric>> newValue = d -> s -> new HasNumeric(s.name, d);
List<HasNumeric> values = List.of(s("a", 17), s("b", 25), s("c", 12), s("d", 5));
List<HasNumeric> hasNumerics = normalisePercentages(values, value, newValue);
System.out.println(hasNumerics.stream().map(HasNumeric::toString).collect(toList()));
List<HasNumeric> fixed = normalisePercentagesWithRounding(values, value, newValue);
System.out.println(fixed.stream().map(HasNumeric::toString).collect(toList()));
}
private static HasNumeric s(String name, double num) {
return new HasNumeric(name, num);
}
static class IndexWrapper<T> implements Comparable<IndexWrapper<T>> {
final T value;
final int index;
IndexWrapper(T value, int index) {
this.value = value;
this.index = index;
}
T getValue() {
return value;
}
<R> IndexWrapper<R> map(Function<T, R> replacer) {
return new IndexWrapper<>(replacer.apply(value), index);
}
static <T> Stream<IndexWrapper<T>> zipWithIndex(Stream<T> in) {
Iterator<T> ts = in.iterator();
Iterator<Integer> indexes = IntStream.iterate(0, i -> i + 1).iterator();
Iterable<IndexWrapper<T>> wrapped = () -> new Iterator<>() {
@Override public boolean hasNext() {
return ts.hasNext();
}
@Override public IndexWrapper<T> next() {
return new IndexWrapper<T>(ts.next(), indexes.next());
}
};
return StreamSupport.stream(wrapped.spliterator(), false);
}
static <T, U extends Comparable<? super U>> Comparator<IndexWrapper<T>> compareContentsWith(Function<T, U> contentsComparator) {
return Comparator.comparing(w -> contentsComparator.apply(w.value));
}
@Override public int compareTo(Hamilton.IndexWrapper<T> o) {
Comparator<IndexWrapper<T>> byIndex = Comparator.comparing(w -> w.index);
return byIndex.compare(this, o);
}
}
static class HasNumeric {
final double num;
final String name;
HasNumeric(String name, double num) {
this.num = num;
this.name = name;
}
@Override public String toString() {
return name + " " + num;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment