com.ocadotechnology.utils.NonNegativeIntegerSplitterByWeights Maven / Gradle / Ivy
/*
* Copyright © 2017-2023 Ocado (Ocava)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ocadotechnology.utils;
import java.util.Comparator;
import java.util.Map.Entry;
import java.util.function.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Table.Cell;
/**
* A class to facilitate dividing a discrete positive quantity amongst different groups according
* to their weights.
*
When an exact assignment cannot be achieved due of the discreteness of the quantity,
* the groups with the highest values will be rounded up.
* @param the type of the groups the quantity will be spit amongst
*/
public class NonNegativeIntegerSplitterByWeights {
private final Comparator> valueComparator = Entry.comparingByValue();
private ImmutableMap proportions;
/**
* Create a splitter based on {@link Double} weights.
* The ordering of the {@link ImmutableMap} may determine the groups to round up in case of weight tie
* (note that {@link ImmutableMap} is deterministic).
*
* @param weights assigned to each group.
*/
public NonNegativeIntegerSplitterByWeights(ImmutableMap weights) {
double totalWeight = weights.values().stream().mapToDouble(w -> w).sum();
proportions = ImmutableMapFactory.createWithNewValues(weights, weight -> weight / totalWeight);
}
/**
* Create a splitter based on {@link Double} weights.
* A reverse comparison between the groups may determine the groups to round up in case of weight tie.
* In case of tie also in the direct comparison, the order of the {@link ImmutableMap} will determine the split
* (note that {@link ImmutableMap} is deterministic).
*
* @param weights assigned to each group.
* @param the type of the groups the quantity will be split amongst
*/
public static > NonNegativeIntegerSplitterByWeights createWithSorting(
ImmutableMap weights) {
ImmutableList orderedKeys = weights.keySet().stream()
.sorted(Comparator.reverseOrder())
.collect(ImmutableList.toImmutableList());
return new NonNegativeIntegerSplitterByWeights<>(ImmutableMapFactory.createFromKeys(orderedKeys, weights::get));
}
/**
* Create a splitter based on {@link Integer} weights.
* The ordering of the {@link ImmutableMap} may determine the groups to round up in case of weight tie
* (note that {@link ImmutableMap} is deterministic).
*
* @param weights assigned to each group.
* @param the type of the groups the quantity will be split amongst
*/
public static NonNegativeIntegerSplitterByWeights createFromInts(ImmutableMap weights) {
return new NonNegativeIntegerSplitterByWeights<>(
ImmutableMapFactory.createWithNewValues(weights, weight -> (double) weight));
}
/**
* Create a splitter based on {@link Integer} weights.
* A reverse comparison between the groups may determine the groups to round up in case of weight tie.
* In case of tie also in the direct comparison, the order of the {@link ImmutableMap} will determine the split
* (note that {@link ImmutableMap} is deterministic).
*
* @param weights assigned to each group.
* @param the type of the groups the quantity will be split amongst
*/
public static > NonNegativeIntegerSplitterByWeights createFromIntsWithSorting(
ImmutableMap weights) {
ImmutableList orderedKeys = weights.keySet().stream()
.sorted(Comparator.reverseOrder())
.collect(ImmutableList.toImmutableList());
return createFromInts(ImmutableMapFactory.createFromKeys(orderedKeys, weights::get));
}
/**
* Split quantity based on {@link ImmutableTable} weights.
* It creates a splitter with integer weights with table cells as keys, splits the quantity
* and return it in {@link ImmutableTable} format.
* A reverse comparison between the groups may determine the groups to round up in case of weight tie.
* In case of tie also in the direct comparison, the order of the {@link ImmutableMap} will determine the split
* (note that {@link ImmutableMap} is deterministic).
* The ordering of the {@link ImmutableTable} may determine the groups to round up in case of weight tie.
*
* @param weights assigned to each group.
* @param numberToSplit quantity to split.
* @param the type of the rows.
* @param the type of the columns.
* @return the quantity split by the table cells.
*/
public static ImmutableTable splitByTableWeights(
ImmutableTable weights,
int numberToSplit) {
ImmutableMap, Double> weightMap = weights.cellSet().stream()
.collect(ImmutableMap.toImmutableMap(
Function.identity(),
Cell::getValue));
ImmutableMap, Integer> split =
new NonNegativeIntegerSplitterByWeights<>(weightMap).split(numberToSplit);
return split.keySet().stream()
.collect(ImmutableTable.toImmutableTable(
Cell::getRowKey,
Cell::getColumnKey,
split::get));
}
/**
* Split quantity based on {@link ImmutableTable} weights.
* It creates a splitter with integer weights with table cells as keys, splits the quantity
* and return it in {@link ImmutableTable} format.
* A reverse comparison between the groups may determine the groups to round up in case of weight tie.
* In case of tie also in the direct comparison, the order of the {@link ImmutableMap} will determine the split
* (note that {@link ImmutableMap} is deterministic).
* The ordering of the {@link ImmutableTable} may determine the groups to round up in case of weight tie.
*
* @param weights assigned to each group.
* @param numberToSplit quantity to split.
* @param the type of the rows.
* @param the type of the columns.
* @return the quantity split by the table cells.
*/
public static ImmutableTable splitByIntegerTableWeights(
ImmutableTable weights,
int numberToSplit) {
ImmutableMap, Integer> weightMap = weights.cellSet().stream()
.collect(ImmutableMap.toImmutableMap(
Function.identity(),
Cell::getValue));
ImmutableMap, Integer> split =
NonNegativeIntegerSplitterByWeights.createFromInts(weightMap).split(numberToSplit);
return split.keySet().stream()
.collect(ImmutableTable.toImmutableTable(
Cell::getRowKey,
Cell::getColumnKey,
split::get));
}
/**
* Split a positive quantity amongst groups according to their weights.
* @param numberToSplit the non negative quantity to split
* @return the group to its share of the numberToSplit map
* @throws IllegalArgumentException if numberToSplit is negative
*/
public ImmutableMap split(int numberToSplit) {
Preconditions.checkArgument(numberToSplit >= 0, "Quantity to split must be non negative");
if (proportions.isEmpty()) {
return ImmutableMap.of();
}
ImmutableMap idealAmounts = ImmutableMapFactory.createWithNewValues(
proportions,
p -> p * numberToSplit);
ImmutableMap roundedDownAmounts = ImmutableMapFactory.createWithNewValues(
idealAmounts,
idealAmount -> (int) Math.floor(idealAmount));
return assignRemainder(numberToSplit, idealAmounts, roundedDownAmounts);
}
private ImmutableMap assignRemainder(
int totalToBeAssigned,
ImmutableMap idealAmounts,
ImmutableMap roundedDownAmounts) {
int summedValues = roundedDownAmounts.values().stream().mapToInt(Integer::intValue).sum();
int totalRemainder = totalToBeAssigned - summedValues;
ImmutableMap remainderByItem = ImmutableMapFactory.createWithNewValues(
idealAmounts,
(item, idealAmount) -> idealAmount - roundedDownAmounts.get(item));
ImmutableSet itemsToAddTo = remainderByItem.entrySet().stream()
.sorted(valueComparator.reversed())
.limit(totalRemainder)
.map(Entry::getKey)
.collect(ImmutableSet.toImmutableSet());
return ImmutableMapFactory.createWithNewValues(
roundedDownAmounts,
(item, amount) -> itemsToAddTo.contains(item) ? Integer.valueOf(amount + 1) : amount);
}
}
| | | |
© 2015 - 2025 Weber Informatics LLC | Privacy Policy