package tc.oc.commons.core.random; import java.util.Map; import java.util.NavigableMap; import java.util.function.Function; import java.util.stream.Stream; import com.google.common.collect.ImmutableSortedMap; import tc.oc.commons.core.stream.Collectors; /** * An immutable chooser. * * Choices and weights are provided at creation time and never change. */ public class ImmutableWeightedRandomChooser<T, N extends Number> extends WeightedRandomChooser<T, N> { /** * Choices indexed by their cumulative weight i.e. the sum of * their own weight and the weights of all choices below them. * * The last key in this map must always be equal to {@link #totalWeight()}. * If the map is empty, then {@link #totalWeight()} must be 0. */ private final NavigableMap<Double, Option> options; private final double totalWeight; public ImmutableWeightedRandomChooser(Stream<T> elements, Function<T, N> scale) { this(elements.collect(Collectors.mappingTo(scale))); } /** * Construct a chooser with the given items * @param weights Map of items to weights */ public ImmutableWeightedRandomChooser(Map<T, N> weights) { final ImmutableSortedMap.Builder<Double, Option> builder = ImmutableSortedMap.naturalOrder(); double total = 0; for(Map.Entry<T, N> entry : weights.entrySet()) { double weight = entry.getValue().doubleValue(); if(weight > 0) { total += weight; builder.put(total, new Option(entry.getKey(), weight)); } } this.options = builder.build(); this.totalWeight = total; } @Override public boolean isEmpty() { return options.isEmpty(); } @Override public double totalWeight() { return totalWeight; } @Override protected T chooseInternal(double n) { final double key = n * totalWeight; // Does random < 1 always imply key < totalWeight? // Not sure, due to rounding return (key < totalWeight ? options.higherEntry(key) : options.lastEntry()) .getValue().element; } }