// 1. List of bowl
const bowlTypes = [
{
name: "A",
description: "holds between 3 and 5 persons worth of food",
min: 3,
max: 5
},
{
name: "B",
description: "holds between 4 to 6 persons worth of food",
min: 4,
max: 6
},
{
name: "C",
description: "holds between 5 and 10 persons worth of food",
min: 5,
max: 10
},
{
name: "D",
description: "holds between 10 and 15 persons worth of food",
min: 10,
max: 15
},
{
name: "E",
description: "holds between 15 and 20 persons worth of food",
min: 15,
max: 20
}
];
// 2. Create a cost function for the best combination of bowls
// e.g. may use sum of the bowls' costs
function getCost(bowls, surplus) {
const total = bowls.reduce((total, { min, max }) => total + ((max - min) / 2), 0);
// penalty for more bowls, heavy penalty for surplus
// adjust function to calibrate, perhaps add actual
// bowl cost to data set
return bowls.length + total + (surplus * surplus);
}
// 3. Evaluate how many bowls we need given a number of persons
function evaluatePersons(persons) {
const bowlCount = bowlTypes.length;
let bestSolution;
// recursive function walking all possible options.
const findSolution = (bowls, servings, startIndex) => {
// while we can add more bowls...
if (servings > 0) {
// try next combination
for (let bowlIndex = startIndex; bowlIndex < bowlCount; ++bowlIndex) {
const bowl = bowlTypes[bowlIndex];
findSolution([ ...bowls, bowl ], servings - bowl.max, bowlIndex);
}
// if the current solution has enough, or too many servings
} else {
// get amount of surplus
const surprlus = Math.abs(servings);
// get the cost of this solution
const cost = getCost(bowls, surprlus);
// if the current solution is better than any previous one
if (!bestSolution || (cost < bestSolution.cost)) {
bestSolution = { bowls, cost, surprlus };
}
}
};
// init first step
for (let bowlIndex = 0; bowlIndex < bowlCount; ++bowlIndex) {
findSolution([], persons, bowlIndex);
}
// optimize solution
bestSolution.bowls = Array.from(bestSolution.bowls.reduce((map, bowl) => {
if (map.has(bowl.name)) {
map.get(bowl.name).qty = map.get(bowl.name).qty + 1;
} else {
map.set(bowl.name, { ...bowl, qty:1 });
}
return map;
}, new Map()).values());
// return our best solution
return bestSolution;
}
// UI for testing purposes
const inputPersons = document.getElementById('inputPersons');
const output = document.getElementById('output');
inputPersons.addEventListener('change', () => {
const solution = evaluatePersons(inputPersons.value);
const verbatim = solution.bowls.map(bowl => `${bowl.qty} x ${bowl.name}: ${bowl.description}`).join('\n');
const debugString = JSON.stringify(solution, null, 3);
output.innerHTML = verbatim + '\n--------\n' + debugString;
});
main {
width: 100%;
display: flex;
flex-direction: column;
}
form {
flex: 0;
}
pre {
min-height: 200px;
flex: 1;
border: 1px solid black;
background-color: #e7e7e7;
padding: 10px;
}
<main>
<form>
<input type="number" id="inputPersons" />
</form>
<pre><code id="output"></code></pre>
</main>