The multiset in this case consists of multiple combinations of menu items that fit into a certain budget. The menu items can be repeated and the permuted combinations are considered the same, e.g. [a,a,b,c]
and [c,a,b,a]
are the same. Such a multiset can be implemented and stored using the Map<String[],Integer>
with the additional filtering methods for representing it as the List<String>
.
Prepare a stream of maps.
Calculate how many times the minimum amount from the map fits in the budget, this is the number of IntStream
iterations.
Prepare a stub of the map of combinations: key - String[]
the array of menu items, value - Integer
the order amount, ¢ cents.
Get a stream of maps Stream<Map<String[],Integer>>
.
Reduce a stream of maps into one map.
Sequentially summarize pairs of maps into one map, adding cheaper menu items to more expensive ones, i.e. sequentially summarize pairs of entries of two maps.
Use the sorted arrays String[]
and TreeMap
with the comparator Arrays::compare
to get rid of duplicates, i.e. permuted arrays.
Use Integer
amounts in cents instead of fractions Double
to avoid inaccuracies when adding the amounts, e.g. 7.949999999999999
or 7.550000000000001
.
Get a map of combinations Map<String[],Integer>
.
Custom filters and representation of the resulting map as a list of strings.
quantity(min,max)
by the number of items in the order.
contains(items)
by the presence of certain items.
minAmount(min)
by the lower bound of the order amount.
get()
the string representation of the combinations map.
Try it online!
class MenuCombinations {
// the combinations of menu items that fit within the budget
private Map<String[], Integer> combinations = Collections.emptyMap();
/**
* @param menu the map of menu items
* @param aBudget the allocated budget, double
*/
public MenuCombinations(Map<String, Double> menu, double aBudget) {
// incorrect incoming data
if (menu == null || menu.size() == 0 || aBudget <= 0) return;
// the allocated budget, ¢ cents
int budget = (int) (aBudget * 100);
// the cheapest menu item, ¢ cents
int min = menu.values().stream()
.mapToInt(val -> (int) (val * 100)).min().orElse(0);
// incorrect incoming data
if (min <= 0) return;
// the stub of the map of combinations
Map<String[], Integer> map = menu.entrySet().stream()
.collect(Collectors.toMap(
// key - the array of menu items
e -> new String[]{e.getKey()},
// value - the order amount, ¢ cents
e -> (int) (e.getValue() * 100)));
// the map of combinations
this.combinations = IntStream.rangeClosed(0, budget / min)
// Stream<Map<String[],Integer>>
.mapToObj(i -> map)
// appending cheaper menu items to more expensive ones
.reduce((map1, map2) -> map1.entrySet().stream()
.flatMap(entry1 -> {
// sum of the chosen menu items
int sum = entry1.getValue();
// if the allocated budget is exceeded
if (sum > budget) return Stream.empty();
// if the allocated budget is reached
if (sum + min > budget)
return Stream.of(Map.ofEntries(entry1));
// otherwise, continue appending menu items
return map2.entrySet().stream()
// skip those items that are greater
.filter(entry2 -> entry2.getValue() + sum <= budget)
// summing two map entries, appending the previous one
.map(entry2 -> Map.of(
// new key - the sorted array of menu items
Stream.of(entry1, entry2)
.map(Map.Entry::getKey)
.flatMap(Arrays::stream)
.sorted() // for comparison
.toArray(String[]::new),
// new value - the order amount, ¢ cents
entry1.getValue() + entry2.getValue(),
// add the previous combination to the new one
entry1.getKey(), entry1.getValue()));
}) // map without duplicates, i.e. permuted arrays
.collect(() -> new TreeMap<>(Arrays::compare),
TreeMap::putAll, TreeMap::putAll))
// otherwise, an empty map
.orElse(Collections.emptyMap());
}
/**
* @param min the minimum number of items in the order, inclusive
* @param max the maximum number of items in the order, inclusive
* @return the representation of filtered combinations
*/
public List<String> quantity(int min, int max) {
return combinations.entrySet().stream()
.filter(entry -> entry.getKey().length >= min
&& entry.getKey().length <= max)
.map(MenuCombinations::entryToString)
.collect(Collectors.toList());
}
/**
* @param items the items that should be present
* @return the representation of filtered combinations
*/
public List<String> contains(Set<String> items) {
return combinations.entrySet().stream()
.filter(entry -> Arrays.asList(entry.getKey())
.containsAll(items))
.map(MenuCombinations::entryToString)
.collect(Collectors.toList());
}
/**
* @param min the lower bound of the order amount, inclusive
* @return the representation of filtered combinations
*/
public List<String> minAmount(double min) {
return combinations.entrySet().stream()
.filter(entry -> entry.getValue() >= (int) (min * 100))
.map(MenuCombinations::entryToString)
.collect(Collectors.toList());
}
/**
* @return the string representation of the combinations map
*/
public List<String> get() {
return combinations.entrySet().stream()
.map(MenuCombinations::entryToString)
.collect(Collectors.toList());
}
@Override
public String toString() {
return combinations.entrySet().stream()
.map(MenuCombinations::entryToString)
.collect(Collectors.joining(", ", "[", "]"));
}
// supplementary method, returns formatted string
private static String entryToString(Map.Entry<String[], Integer> e) {
return String.format("%s=%d.%s", Arrays.toString(e.getKey()),
e.getValue() / 100, (e.getValue() % 100 + "00").substring(0, 2));
}
}
public static void main(String[] args) {
Map<String, Double> menu = Map.of(
"Fruit", 2.15, "Fries", 2.75, "Salad", 3.35,
"Wings", 3.55, "Mozzarella", 4.20, "Plate", 5.80);
System.out.println(new MenuCombinations(menu, 4.30).quantity(2, 2));
System.out.println(new MenuCombinations(menu, 5.5).minAmount(5.5));
System.out.println(new MenuCombinations(menu, 2.15));
System.out.println(new MenuCombinations(menu, 8.60).quantity(4, 4));
System.out.println(new MenuCombinations(menu, 9.2).contains(Set.of("Plate")));
System.out.println("Map of combinations for a budget of: 8.50");
new MenuCombinations(menu, 8.5).get().forEach(System.out::println);
}
Output:
[[Fruit, Fruit]=4.30]
[[Fries, Fries]=5.50, [Fruit, Salad]=5.50]
[[Fruit]=2.15]
[[Fruit, Fruit, Fruit, Fruit]=8.60]
[[Fries, Plate]=8.55, [Fruit, Plate]=7.95, [Plate]=5.80, [Plate, Salad]=9.15]
Map of combinations for a budget of: 8.50
[Fries]=2.75
[Fries, Fries]=5.50
[Fries, Fries, Fries]=8.25
[Fries, Fries, Fruit]=7.65
[Fries, Fruit]=4.90
[Fries, Fruit, Fruit]=7.50
[Fries, Fruit, Salad]=8.25
[Fries, Fruit, Wings]=8.45
[Fries, Mozzarella]=6.95
[Fries, Salad]=6.10
[Fries, Wings]=6.30
[Fruit]=2.15
[Fruit, Fruit]=4.30
[Fruit, Fruit, Fruit]=6.45
[Fruit, Fruit, Mozzarella]=8.50
[Fruit, Fruit, Salad]=7.65
[Fruit, Fruit, Wings]=7.85
[Fruit, Mozzarella]=6.35
[Fruit, Plate]=7.95
[Fruit, Salad]=5.50
[Fruit, Wings]=5.70
[Mozzarella]=4.20
[Mozzarella, Mozzarella]=8.40
[Mozzarella, Salad]=7.55
[Mozzarella, Wings]=7.75
[Plate]=5.80
[Salad]=3.35
[Salad, Salad]=6.70
[Salad, Wings]=6.90
[Wings]=3.55
[Wings, Wings]=7.10
See also: Integer partition iterative code optimization