Suggested sites

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))))

blog comments powered by Disqus