Tutorial 5: The Intelligent Fridge
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.
- Recipes: crepes.txt, flan.txt, madeleines.txt;
- Supermarkets: market1.txt, market2.txt, market3.txt;
- Fridge contents: fridge1.txt, fridge2.txt, fridge3.txt;
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:
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
, whereingredient
is a string andamount
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
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:
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:
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:
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:
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:
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:
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:
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: