This assignment has been closed on November 14, 2023.
You must be authenticated to submit your files

Tutorial 5: The Intelligent Fridge

G. Pogudin apres S. Mengel

Objectives

What we will practice today: Operations on dictionaries, tuples and lists; reading and writing files.

Setup: before you start

Launch Spyder (from the Applications/Programming menu). If you see a “Spyder update” message, just click OK and ignore it.

Before you start: create a new project, called Tutorial_5 (using “New Project” from the “Projects” menu). This will ensure that files from last week (and future weeks) do not get mixed up together.

Now download the following files by right-clicking them, choosing Save as, and saving them to your Tutorial_5 folder.

Check and make sure that they have all appeared in your Tutorial_5 project in Spyder. Afterwards, create a new file and call it shopping.py. This is the file that you will be writing your program in today.

Exercises

Today’s tutorial is about cooking, and buying ingredients, intelligently - with the help of your “smart” refrigerator.

Exercise 1: Printing a Recipe

We will store recipes in dictionaries, whose keys are ingredient names and whose values are corresponding amounts, specifying how much of each ingredient is necessary for cooking the given dish.

For example, here is a recipe for (plain) crepes:

In [1]: crepes = {'wheat flour': 250, 'milk': 50, 'egg': 4, 'butter': 50, 'salt': 1}

(We leave the interpretation of the amounts up to the user; for example, the above should mean 250 grams of wheat flour, 50 milliliters of milk, etc.)

We want to be able to print out such a recipe, so that the cook can read it easily.

Define a function print_recipe which takes as input a recipe and prints it so that ingredients and amounts are separated by a colon followed by a single space.

def print_recipe(recipe):
    """Pretty print recipe, which is a dictionary whose keys are
    ingredients and whose values are their corresponding amounts.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [2]: crepes = {'wheat flour': 250, 'milk': 50, 'egg': 4, 'butter': 50, 'salt': 1}

In [3]: print_recipe(crepes)
salt: 1
milk: 50
wheat flour: 250
butter: 50
egg: 4

In [4]: tartiflette = {'potato': 750, 'reblochon': 400, 'lardon': 200, 'onions': 3}

In [5]: print_recipe(tartiflette)
potato: 750
reblochon: 400
lardon: 200
onions: 3

Note that the order of lines in your output may be different. (Why is that?)

Hint: in this TD, you will often iteratre over dictionaries. An elegant way of doing this is to use the construction for k, v in d.items(), in which k and v will iterate through the keys and values of dictionary d, respectively.

Test your function:

  • Try to replicate the examples above.
  • Test it on a few other examples.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 2: Reading a Recipe from a File

Rather than typing in recipes by hand each time, we want to keep and use a collection of recipe files (each listing the ingredients and amounts for a recipe in plain text format). We therefore need a function which can read a list of ingredients from a recipe file.

First, let us define the format for recipe files. Recipe files are plain text files, the lines are as follows:

  • some lines may be completely empty.
  • each line that is not empty contains exactly one pair ingredient,amount, where ingredient is a string and amount is a dimensionless integer;
  • the ingredients and amounts may be surrounded by extra tabs or spaces;
  • no ingredient ever contains ',' (so there is exactly one comma per non-empty line, and it appears between the ingredient and the amount).
  • no ingredient appears more than once in the file.

For example, the file crepes.txt has the following content:

wheat flour , 250
         egg,4

milk, 50
  butter ,  50
salt,1

Define a function read_recipe which takes as input the name of a recipe file and returns the recipe as a dictionary. Remember that the amounts of the ingredients in the recipe should be integers, not strings!

Your function should start as follows:

def read_recipe(recipe_file_name):
    """Read recipe file 'recipe_file_name', and return ingredients as a
    dictionary whose keys are ingredients and whose values are the
    corresponding amounts.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [6]: read_recipe('flan.txt')
Out[6]: {'egg': 2, 'cornstarch': 50, 'milk': 500, 'vanilla': 15, 'sugar': 150}

In [7]: read_recipe('crepes.txt')
Out[7]: {'salt': 1, 'butter': 50, 'milk': 50, 'egg': 4, 'wheat flour': 250}

Note that the ordering of items may be different for you.

Hint: functions strip and split (in particular, s.split(',')) you used in the previous TD may be very helpful here!

Test your function:

  • Try to replicate the examples above and
  • Once your function looks correct in the manual tests, upload your file shopping.py:
    Upload form is only available when connected

Exercise 3: Writing a Recipe to a File

We will also need to write recipes to files. Define a function write_recipe which takes as input a recipe dictionary and a file name and writes the dictionary to the file.

Your function should start as follows:

def write_recipe(recipe, recipe_file_name):
    """Write recipe to a file named recipe_file_name."""
    pass  # remove this line and replace with your own code

In the console, we expect that the commands

In [8]: flan = read_recipe('flan.txt')

In [9]: write_recipe(flan, 'flan2.txt')

will create a new file flan2.txt with the contents

milk,500
egg,2
cornstarch,50
vanilla,15
sugar,150

Note that the order of items may be different for you. Test your function by replicating the example above and trying to do the same for crepes and madeleines.

Upload your file shopping.py

Upload form is only available when connected

Exercise 4: Introducing Fridges

We want to find out whether we can cook a given dessert with the ingredients which we have in our fridge. The contents of our fridge is given as a fridge file fridge1.txt. Fridge files are like recipe files, except that ingredients may appear more than once. (For example, your fridge might have three bottles of milk: one full half-litre, another with only 200ml left in it, and a third with only 100ml.)

For example, the given file fridge1.txt has the following content:

milk,500
egg,3
milk,200
cornstarch,50
salt,5
milk,100
butter,100
salt,5
sugar,200
vanilla,20

Define a function read_fridge which takes as input the name of a fridge file and returns the fridge content as a dictionary. All amounts of the same ingredient should be combined! You may want to start by copying and then modifying your read_recipe function.

Your read_fridge function should start as follows:

def read_fridge(fridge_file_name):
    """Read fridge file 'fridge_file_name', and return the ingredients
    held in the given fridge as an ingredient=amount dictionary.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [10]: read_fridge('fridge1.txt')
Out[10]:
{'salt': 10,
'vanilla': 20,
'sugar': 200,
'egg': 3,
'milk': 800,
'butter': 100,
'cornstarch': 50}

In [11]: read_fridge('fridge2.txt')
{'milk': 800, 
'egg': 3, 
'cornstarch': 50, 
'salt': 10, 
'butter': 100, 
'sugar': 200, 
'vanilla': 20, 
'wheat flour': 250}

As before, the ordering of items may be different for you.

Test your function by replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 5: Can I Cook a Given Dish?

Now, define a function is_cookable which takes as input a recipe file name and a fridge file name, and returns True if we have sufficient ingredients in our fridge to cook the dish specified in the recipe; otherwise it returns False.

Your function should start as follows:

def is_cookable(recipe_file_name, fridge_file_name):
    """Return True if the contents of the fridge named fridge_file_name
    are sufficient to cook the recipe named recipe_file_name.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [12]: is_cookable('crepes.txt', 'fridge1.txt')
Out[12]: False

In [13]: is_cookable('flan.txt', 'fridge1.txt')
Out[13]: True

Test your function replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 6: Adding up recipes

We would like to know what ingredients to buy for a given list of desserts which we want to cook. We achieve this in two steps. First, define a function add_recipes which takes as input a list of recipe dictionaries, adds up all the ingredients, and returns them in a dictionary.

Your function should start as follows:

def add_recipes(recipes):
    """Return a dictionary representing the sum of all of
    the recipe dictionaries in recipes.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior (again, the order of your lines may differ):

In [14]: add_recipes([read_recipe('crepes.txt'), read_recipe('flan.txt')])
Out[14]:
{'salt': 1,
'milk': 550,
'vanilla': 15,
'egg': 6,
'sugar': 150,
'wheat flour': 250,
'butter': 50,
'cornstarch': 50}

In [15]: add_recipes([read_recipe('flan.txt'), read_recipe('madeleines.txt')])
Out[15]: 
{'egg': 4, 
'milk': 500, 
'sugar': 250, 
'cornstarch': 50, 
'vanilla': 20, 
'butter': 125, 
'wheat flour': 100, 
'yeast': 1, 
'almond': 20}

Test your function replicating the examples above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 7: Creating a Shopping List

Now, define a function create_shopping_list which takes as input a list of recipe files and a fridge file and returns a dictionary of ingredients which we need to buy.

Your function should start as follows:

def create_shopping_list(recipe_file_names, fridge_file_name):
    """Return the shopping list (a dictionary of ingredients and
    amounts) needed to cook the recipes named in recipe_file_names,
    after the ingredients already present in the fridge named
    fridge_file_name have been used.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following output (perhaps with a different ordering of items):

In [16]: create_shopping_list(['crepes.txt', 'flan.txt'], 'fridge1.txt')
Out[16]: {'egg': 3, 'wheat flour': 250}

In [17]: create_shopping_list(['crepes.txt', 'flan.txt'], 'fridge2.txt')
Out[17]: {'egg': 3}

In [18]: create_shopping_list(['crepes.txt', 'flan.txt'], 'fridge3.txt')
Out[18]: {'wheat flour': 250, 'egg': 1}

Test your function by replicating the examples above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 8: Computing Total Prices

We want a function to help us decide where we should go to buy the items on our shopping list. To this end, three files are provided which detail the prices in different supermarkets, market1, market2 and market3. The files are organized using ingredient,value lines, precisely like recipe files, but now value stands for the price of the ingredient in a hypothetical currency of millicents per unit.

Define a function total_price which, given a shopping list and a supermarket name, returns the total cost of buying the given items in this supermarket. (We are assuming that the supermarkets always contain all the ingredients needed).

Your function should start as follows:

def total_price(shopping_list, market_file_name):
    """Return the total price in millicents of the given shopping_list
    at the market named market_file_name.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following output:

In [19]: todays_menu = ['crepes.txt', 'flan.txt', 'madeleines.txt']

In [20]: what_we_need = create_shopping_list(todays_menu, 'fridge1.txt')

In [21]: total_price(what_we_need, 'market1.txt')
Out[21]: 158200

In [22]: what_we_need = create_shopping_list(todays_menu, 'fridge2.txt')

In [23]: total_price(what_we_need, 'market1.txt')
Out[23]: 108200

In [24]: what_we_need = create_shopping_list(todays_menu, 'fridge3.txt')

In [25]: total_price(what_we_need, 'market2.txt')
Out[25]: 144855

Test your function by replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 9: Where Should I Shop?

Now, define a function find_cheapest which, given a shopping list and a list of supermarkets, outputs the supermarket in which the total cost of our shopping would be lowest and what it would cost to shop there. As before, we are assuming that the supermarkets always contain all the ingredients needed.

Your function should start as follows:

def find_cheapest(shopping_list, market_file_names):
    """Return the name of the market in market_file_names
    offering the lowest total price for the given shopping_list,
    together with the total price.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [26]: todays_menu = ['crepes.txt', 'flan.txt', 'madeleines.txt']

In [27]: what_we_need = create_shopping_list(todays_menu, 'fridge1.txt')

In [28]: supermarkets = ['market1.txt', 'market2.txt', 'market3.txt']

In [29]: find_cheapest(what_we_need, supermarkets)
Out[29]: ('market2.txt', 144925)

Test your function replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 10: Putting Things Together

We now put things together into a function which tells us what and where to shop (and how much this will cost) and refills our fridge accordingly.

Define a function update_fridge which

  • takes as input the names of a fridge file, a list of recipe files, a list of supermarket files, and a new-fridge file;
  • prints out a shopping list, together with the supermarket where we should go, and how much the total cost will be;
  • and finally writes a new-fridge file, containing the contents of the fridge after shopping (and before cooking!).

Your function should start as follows:

def update_fridge(fridge_file_name, recipe_file_names, market_file_names, new_fridge_file_name):
    """Compute the shopping list for the given recipes after the
    ingredients in fridge fridge_file_name have been used; find the cheapest
    market; and write the new fridge contents to new_fridge_file_name.
    Print the shopping list, the cheapest market name, and the total
    amount to be spent at that market.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [30]: todays_menu = ['crepes.txt', 'flan.txt', 'madeleines.txt']

In [31]: supermarkets = ['market1.txt', 'market2.txt', 'market3.txt']

In [32]: update_fridge('fridge1.txt', todays_menu, supermarkets, 'fridge_new.txt')
Shopping list:
butter: 75
sugar: 50
yeast: 1
egg: 5
wheat flour: 350
almond: 20
Market: market2.txt
Total cost: 144925

(perhaps in a different order). Additionally, a file fridge_new.txt should have been created, with the content:

milk,800
wheat flour,350
egg,8
cornstarch,50
salt,10
butter,175
sugar,250
yeast,1
vanilla,20
almond,20

Test your function by replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected

Exercise 11 (optional): Distributed Shopping

We can save money if we buy every item on our shopping list in the supermarket where it is cheapest. We hence want to split our shopping list into a list of shopping lists, one for each supermarket, so that we buy each item where it is cheapest.

Define a function distributed_shopping_list which takes as input a shopping list and a list of supermarket names, and returns a dictionary of shopping lists, one for each supermarket.

Your function should start as follows:

def distributed_shopping_list(shopping_list, market_file_names):
    """Distribute shopping_list across the markets named in market_file_names
    to minimize the total cost.
    """
    pass  # remove this line and replace with your own code

In the console, we expect the following behavior:

In [33]: todays_menu = ['crepes.txt', 'flan.txt', 'madeleines.txt']

In [34]: what_we_need = create_shopping_list(todays_menu, 'fridge1.txt')

In [35]: supermarkets = ['market1.txt','market2.txt','market3.txt']

In [36]: distributed_shopping_list(what_we_need, supermarkets)
Out[36]:
{'market3.txt': {'egg': 5, 'wheat flour': 350},
'market1.txt': {'yeast': 1, 'butter': 75, 'almond': 20, 'sugar': 50},
'market2.txt': {}}

(perhaps in a different order).

Test your function by replicating the example above.

Upload your file shopping.py:

Upload form is only available when connected