phpnested-sets

How to calculate the amount of each ingredient?


I'm trying to solve a problem, where I need to calculate nutrition values of a recipe like a "My sandwich"

I have a database with "recipes" and "ingredients" and the amount needed in kg.

a recipe can contain ingredients but also other recipes which then again can contain recipes/ingredients.

This is an example of a "part list" of a "Test Sandwich" with a total weight of 0.095 kg.

array (
  0 => 
  array (
    'id' => 2,
    'name' => 'Test Cheese',
    'metric_id' => 1,
    'category_id' => NULL,
    'quantity' => 0.015,
    'depth' => 1,
  ),
  1 => 
  array (
    'id' => 3,
    'name' => 'Test Ham',
    'metric_id' => 1,
    'category_id' => NULL,
    'quantity' => 0.03,
    'depth' => 1,
  ),
  2 => 
  array (
    'id' => 4,
    'name' => 'Test Sesam Bread',
    'metric_id' => 1,
    'category_id' => NULL,
    'quantity' => 0.05,   // Only use some of the test bread (with added Sesam)
    'depth' => 1,
    'parts' => 
    array (
      0 => 
      array (
        'id' => 5,
        'name' => 'Test Bread',
        'metric_id' => 1,
        'category_id' => NULL,
        'quantity' => 1.0,         // This a recipe for 1 kg of banana bread 
        'depth' => 2,
        'parts' => 
        array (
          0 => 
          array (
            'id' => 6,
            'name' => 'Test Flour',
            'metric_id' => 1,
            'category_id' => NULL,
            'quantity' => 0.5,
            'depth' => 3,
          ),
          1 => 
          array (
            'id' => 7,
            'name' => 'Test Water',
            'metric_id' => 1,
            'category_id' => NULL,
            'quantity' => 0.3,
            'depth' => 3,
          ),
          2 => 
          array (
            'id' => 8,
            'name' => 'Test Yeast',
            'metric_id' => 1,
            'category_id' => NULL,
            'quantity' => 0.05,
            'depth' => 3,
          ),
          3 => 
          array (
            'id' => 9,
            'name' => 'Test Banan',
            'metric_id' => 1,
            'category_id' => NULL,
            'quantity' => 0.15,
            'depth' => 3,
          ),
        ),
      ),
      1 => 
      array (
        'id' => 10,
        'name' => 'Test Sesam Seeds',
        'metric_id' => 1,
        'category_id' => NULL,
        'quantity' => 0.1,
        'depth' => 2,
      ),
    ),
  ),
)

How can I reduce such a structure to a list of amount/weight of each individual "end" ingredients, like flour, water, bananas, cheese, ham, sesam seeds etc. ?

I written this function to run through the structure, but it's not really doing the job (seems like it calculate the total weight correctly)

function resolveAssembly($items, $number = 1, $declaration = [])
{
    $weight = 0;

    foreach ($items as $item) {
        // Get the quantity (weight) of the item
        $amount = $item['quantity'];

        // Check if the item also contains an ingredients list
        if (array_key_exists('parts', $item)) {

            // Get the total weight of the whole assembly
            list($assembly_weight, $assembly_declaration) = resolveAssembly($item['parts'], $number * $amount, $declaration);

            $declaration = array_merge($declaration, $assembly_declaration);

            // Calculate the relative weight of each part in the assembly
            foreach ($item['parts'] as $part) {
                $weight = $weight + ($amount / $assembly_weight) * $part['quantity'];
            }
        } else {
            // Add the amount to the declaration
            if (!array_key_exists($item['name'], $declaration)) {
                $declaration[$item['name']] =  $number * $amount;
            }

            $weight = $weight + $amount;
        }
    }

    return [$weight, $declaration];
}

Edit: merging assembly declaration array

This is how I use the above function:

$item = Inventory::findOrFail(1); // Get "Test sandwich"
$parts = $item->getAssemblyItemsList(); // Get the parts of the "Test sandwich"
$result = resolveAssembly($parts, 1); // Get total weight of 1 "Test sandwich" + a list of amount/weight of each ingredient

Current outcome of the resolveAssembly() function:

0.095
[
  "Test Cheese" => 0.015
  "Test Ham" => 0.03
  "Test Flour" => 0.025
  "Test Water" => 0.015
  "Test Yeast" => 0.0025
  "Test Banan" => 0.0075
  "Test Sesam Seeds" => 0.005 // Should have been factored in the bread ingredients
]

But it's not quite right, it should add up to 0.095, so the Test Sesam Seeds isn't factored in the stuff the bread is made up of (flour, yeast, banana etc.) I think.

0.015+0.03+0.025+0.015+0.0025+0.0075+0.005 = 0.1 kg of "Test Sandwich"

Also if this helps, I'm using this system to store these recipes: https://github.com/stevebauman/inventory/blob/v1.7/docs/ASSEMBLIES.md (but modified to store decimal numbers and also negative numbers (e.g. to handle water lost or added doing a cooking process))

And here is all the calculations done to sum the total weight of the "Test Sandwich":

"Test Cheese: 0 + 0.015 = 0.015"

"Test Ham: 0.015 + 0.03 = 0.045"

"Test Flour: 0 + 0.5 = 0.5"

"Test Water: 0.5 + 0.3 = 0.8"

"Test Yeast: 0.8 + 0.05 = 0.85"

"Test Banan: 0.85 + 0.15 = 1"

"Test Flour: 0 + (1 / 1) * 0.5 = 0.5"

"Test Water: 0.5 + (1 / 1) * 0.3 = 0.8"

"Test Yeast: 0.8 + (1 / 1) * 0.05 = 0.85"

"Test Banan: 0.85 + (1 / 1) * 0.15 = 1"

"Test Sesam Seeds: 1 + 0.1 = 1.1"

"Test Bread: 0.045 + (0.05 / 1.1) * 1 = 0.09045454545"

"Test Sesam Seeds: 0.090454545454545 + (0.05 / 1.1) * 0.1 = 0.095"

Solution

  • This should get you closer:

    function resolveAssembly($items) {
        $weight = 0;
        $declaration = [];
        foreach ($items as $item) {
            $weight += $item['quantity'];
            if (array_key_exists('parts', $item)) {
                // if there are parts, get the full weight and declaration for the sub item
                list($sub_total_weight, $sub_declaration) = resolveAssembly($item['parts']);
                foreach ($sub_declaration as $sub_name => $sub_weight) {
                    if (!array_key_exists($sub_name, $declaration)) {
                        $declaration[$sub_name] = 0;
                    }
                    // Total weight of particular ingredient is the sub items ingredient out of the sub items full weight
                    // times the quantity for this particular item
                    $declaration[$sub_name] += $item['quantity'] * ($sub_weight / $sub_total_weight);
                }
            } else {
                // if there are no parts, just add the quantity
                if (!array_key_exists($item['name'], $declaration)) {
                    $declaration[$item['name']] = 0;
                }
                $declaration[$item['name']] += $item['quantity'];
            }
        }
        return [$weight, $declaration];
    }
    

    This will give you this result:

    array(2) {
        [0]=>
      float(0.095)
      [1]=>
      array(7) {
            ["Test Cheese"]=>
        float(0.015)
        ["Test Ham"]=>
        float(0.03)
        ["Test Flour"]=>
        float(0.022727272727273)
        ["Test Water"]=>
        float(0.013636363636364)
        ["Test Yeast"]=>
        float(0.0022727272727273)
        ["Test Banan"]=>
        float(0.0068181818181818)
        ["Test Sesam Seeds"]=>
        float(0.0045454545454545)
      }
    }
    

    If you add up the numbers above, they should total 0.095.

    Basically when you're doing a 'sub recipe' you need to know what the total 'weight' of that recipe is, and from there you can figure out the actual items total based on that sub's quantity.