Recipes

Ever looked into the kitchen cupboard and wondered what you can make for supper? Then this program is the answer. You first select the ingredients you have available:

ingredients.png

It then tells you the recipes you can make:

recipes.png

You can add new recipes to the database, and update the list of ingredients.

Complete listing

Description

The recipe program uses two global variables. The first one, ingredient-database, contains a list of all the ingredients we are going to use:

(defparameter ingredient-database '("eggs" "flour" "butter" "chicken" "beef" "pork"
"lamb" "sugar" "chocolate" "onions" "fish" "tomatoes" "pasta" "chorizo" "rice"
"potatoes" "cheese"))

The second one, recipe-database, is going to contain a list of the recipes. Each recipe is a list of three items:

  • The name of the recipe.
  • A list of the ingredients.
  • A short description of the method.

So after a couple of recipes have been added, recipe-database might look like this:

(("Cheese omelette" ("eggs" "cheese")
"Beat the eggs, cook, and add the cheese")
("Pizza" ("flour" "tomatoes" "chorizo" "cheese")
"Make a dough, add tomato, cheese, and chorizo, and bake for 12m at 210°C"))

Basic routines

Our starting point is a procedure contains to test whether an ingredient is contained in a list of ingredients. We will use it like this:

CL-USER > (contains "chicken" '("pork" "chicken" "rice"))
T

but:

CL-USER > (contains "chicken" '("pork" "chocolate" "rice"))
NIL
The definition in English is as follows:

To see if an ingredient is in a list of ingredients:

  • If the list is empty then the answer is no.
  • If the ingredient is the first item on the list then the answer is yes.
  • Otherwise it's the answer to the question - is the ingredient in the rest of the ingredients excluding the first element?

Here's the definition as a Lisp procedure:

(defun contains (item list)
  (if (null list) nil
    (if (string= item (first list)) t
      (contains item (rest list)))))

Based on this we define a procedure subset that checks whether a list of ingredients lista is a subset of the list listb. We will use this as follows:

CL-USER > (subset '("pork" "rice") '("pork" "eggs" "rice"))
T

But:

CL-USER > (subset '("pork" "eggs" "rice") '("pork" "rice"))
NIL

In English this is defined as follows:

To check whether lista is a subset of listb

  • If lista is empty then the answer is yes.
  • If listb doesn't contain the first element of lista then the answer is no
  • Otherwise it's the answer to the question - is the rest of lista excluding the first element a subset of listb?

As a Lisp procedure it becomes:

(defun subset (lista listb)
  (if (null lista) t
    (if (null (contains (first lista) listb))
        nil
      (subset (rest lista) listb))))

The recipe program

Now we're going to define the procedure that finds all the recipes you can make with a particular set of ingredients. We will call it like this:

CL-USER > (recipes-can-make '("cheese" "eggs") recipe-database)
(("Cheese omelette" ("eggs" "cheese") "Beat the eggs, cook, and add the cheese"))

The definition in English is:

To find the recipes you can make with the ingredients:

  • If the list of recipes is empty then answer none.
  • If the first recipe's ingredients are a subset of the list of available ingredients, return that recipe plus the result of checking the remaining recipes.
  • Otherwise return just the result of checking the remaining recipes.

Here's the procedure in Lisp:

(defun recipes-can-make (ingredients recipes)
  (if (null recipes) nil
    (let* ((entry (first recipes))
           (needs (second entry)))
      (if (subset needs ingredients)
          (cons entry (recipes-can-make ingredients (rest recipes)))
        (recipes-can-make ingredients (rest recipes))))))

The user interface

Finally we add some dialogue boxes to make adding and looking up recipes easier. Here's a procedure for adding a recipe:

(defun add-recipe ()
  (let ((name (capi:prompt-for-string "What's the recipe?"))
        (ingredients (capi:prompt-for-items-from-list
                      ingredient-database 
                      "What does it need?"))
        (method (capi:prompt-for-string "Brief method:")))
    (setq recipe-database (cons (list name ingredients method) recipe-database))))

Now here's the interface for looking up a recipe:

(defun find-recipe ()
  (let ((ingredients (capi:prompt-for-items-from-list 
                      (sort ingredient-database #'string<) 
                      "What ingredients do you have?")))
    (capi:prompt-with-list
     (recipes-can-make ingredients recipe-database) 
     "You can make these:")))

Saving and loading the databases

Finally, here are procedures to save the databases to a file on disk:

(defun save-recipes ()
  (with-open-file (stream "Recipes" :direction :output :if-exists :supersede)
    (write ingredient-database :stream stream)
    (write recipe-database :stream stream)))

and load them back in:

(defun load-recipes ()
  (with-open-file (stream "Recipes" :direction :input)
    (setf ingredient-database (read stream))
    (setf recipe-database (read stream))))

Previous: Random Limericks

Next: Map


blog comments powered by Disqus