Last active
October 22, 2017 19:25
-
-
Save chrismay/82ad875b508d9aabea6c0a078e6135b0 to your computer and use it in GitHub Desktop.
An implementation of Hamilton's method for Smallest-Remainder rounding of percentages.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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