OutFriday, October 16, 2026
DueFriday, October 23, 2026, 7:00 pm

Overview

A company sells weekly meal kits. A customer sets up a meal plan indicating which recipes they want to make each day and for how many people. The company needs to aggregate this into one shopping list that spans all the recipes. In this assignment, you'll write the code to do this by cleaning each recipe once, keeping the clean recipes in a recipe book, and then scaling and combining recipes from the book, using a combination of table and list operations. You'll also practice testing functions that return tables using properties.

Learning objectives

After this assignment, you should be able to:

AI and collaboration policy for this assignment

This assignment has a mix of manual and AI activities. You will use an AI agent to generate shopping-list (Task 5). You may also use AI to copy your check blocks to your hw5-tests file, as described towards the end of the document.

By now, we assume that you've gotten comfortable with the idea of writing code plans. The handout does not ask you to write them, but we encourage you to use them, as well as drawings of tables and where the values will come from, to help you with the problems. If you come to hours, we'll ask to see some sort of planning artifact so we can help diagnose where you are stuck.

Setup

The CS111 tables documentation lists the table operations in the dcic2024 context. For lists, you can use filter, map, distinct, length, member, all, any, first, and append. Post to Ed if you think you need something else.

Part 1The Data

A recipe is a table with three columns: the ingredient, the amount, and the unit. Every recipe is written to serve base-servings (4) people. We have given you a google sheet with multiple recipes in separate tabs.

ingredient | amount | unit
---------------------------
Chickpeas  | 2      | cup
garlic     | 2      | clove
Olive Oil  | 3      | tbsp

The meal plan says what to cook this week: the day, which recipe, and how many servings are needed. A recipe can appear more than once in the same meal plan (on different days).

day      | recipe-ref | servings-needed
---------------------------------------
Monday   | dal        | 4
Tuesday  | hummus     | 2

In order to aggregate recipes, we'll need to have the recipes using the same units. The starter code gives you a table called units that indicates how to convert common units into a smaller set (such as "tbsp" to "tsp"). If you write your own test recipes, stick to units in the first column of the units table. There is also a table called densities that converts from grams to teaspoons for common ingredients.

unit | base | per-base          ingredient | g-per-tsp
-----------------------         ------------------------
tsp  | tsp  | 1                 salt       | 6
tbsp | tsp  | 3                 cumin      | 2
kg   | g    | 1000

Part 2Normalizing a Recipe

The recipes were typed by different people, so the data have inconsistencies. Names vary in case ("Onion" vs. "onion"). Salt is in teaspoons in one recipe, tablespoons in another, and grams in a third, and one recipe is entirely metric.

In order to combine recipes, we first need them to use standard capitalization on ingredient names and standard units for the same ingredients. For this part, you'll write a function that takes a recipe and puts it in standard form.

Task 1

Write a function

fun normalize-recipe(recipe :: Table) -> Table:
  ...
end
that takes a recipe table and produces a new recipe table in which:
  • the ingredient name is in lowercase, and
  • the amount and unit columns are adjusted as needed to convert the units to the standard one for that ingredient

Create helper functions as you think makes sense to keep your code readable and testable in small pieces. The lookup-cell function from the starter file will come in handy, and so will map over the rows of the table (see the helper below).

Raise an "unknown unit" error if a recipe uses a unit that isn't in the units table.

Example: a two-row recipe (the olive oil and salt rows of hummus):

ingredient | amount | unit               ingredient | amount | unit
--------------------------    becomes    --------------------------
Olive Oil  | 3      | tbsp               olive oil  | 9      | tsp
Salt       | 0.5    | tbsp               salt       | 9      | g
Helper for this task: build-table-from-rows

The starter file gives you build-table-from-rows :: (List<String>, List<List>) -> Table. Its first argument is the column names and its second is a list of rows, each written as a list of values in column order. For example, build-table-from-rows([list: "a", "b"], [list: [list: 1, "x"], [list: 2, "y"]]) is a table with columns a and b and two rows. Use lowered.all-rows() (the rows of a table, as a list) with map to compute the new rows, then hand them to this function.

Part 3Building a Recipe Book

The company processes many meal plans, and they all draw on the same recipes (dal even appears twice in meal-plan). We shouldn't load and clean a recipe every time a plan uses it. Instead, we'll clean every recipe on the sheet once and keep the results in a recipe book: a list of pages, where each page pairs a recipe's name with its normalized table. We can then reuse the book for every meal plan. The starter file defines the pages for you, and gives you recipe-names, the list of tab names on the sheet:

data Page:
  | page(recipe-name :: String, recipe :: Table)
end

Task 2

Write a function

fun load-recipe-book(names :: List<String>) -> List<Page>:
  ...
end
that returns a recipe book with one page for each name in names. Each page holds the recipe on that sheet tab, loaded and normalized. For recipe-names, the book has 6 pages. Create the book once, as book = load-recipe-book(recipe-names), and pass it to the functions below.

Helper for this task: load-recipe-from-sheets

The starter file gives you load-recipe-from-sheets :: String -> Table. It returns the recipe on the sheet tab with that name, for example load-recipe-from-sheets("dal"). The names in recipe-names, and in the meal plan's recipe-ref column, are tab names.

Task 3

Write a function

fun find-recipe(book :: List<Page>, name :: String) -> Table:
  ...
end
that returns the recipe on the page of book named name. Raise an error if the book has no such page.

Part 4Scaling Recipes to the Number of Servings

Task 4

Write a function

fun scale-recipe(recipe :: Table, servings :: Number) -> Table:
  ...
end
that takes a normalized recipe and scales the amount to the number of servings. Remember that all recipes were written for four servings, so you might have to adjust amounts up or down depending on whether the desired number of servings is less than or greater than four. For example, scaling normalize-recipe(dal) to 8 servings gives 400 for the lentils (200 g).

Part 5Creating a Shopping List

Task 5

Use an AI agent to write a function

fun shopping-list(plan :: Table, book :: List<Page>) -> Table:
  ...
end
that takes a meal plan and a recipe book and produces the shopping list for the whole plan: a table with columns ingredient, total, and unit, with one row for each distinct ingredient. The rows must be sorted alphabetically (A to Z) by ingredient name, so that every correct answer produces the same table. You may assume that after normalization every ingredient is measured in a single unit. For meal-plan, garlic is 10 cloves and salt is 36 g, and the list has 16 rows.

Give the agent this description and your starter file, and tell it to use only the table and list operations listed in Setup. Paste its function into hw5-code.arr and confirm that it produces the values above.

Then read the code and label it. Above each step, write a comment saying what that step computes, including the columns and rows of any intermediate table. Also say in comments which step groups the rows by ingredient, and which step makes the order canonical. If you don't understand a step, ask the agent to explain it, then write the explanation in your own words. You are responsible for every line you hand in. You will test the function with your own property checkers in Task 7.

Part 6Property Tests

Let's practice property-based testing a bit more. Here are three properties that we might want on the functions we asked you to write:

Task 6

For each of these properties, write a function that takes the necessary inputs and checks the corresponding relationship. Here are the function headers (which are also in the starter file):

fun check-same-ingredients(recipe1 :: Table, recipe2 :: Table) ->
Boolean:
  doc: ```determine whether the recipes have the same ingredients, not
  necessarily in the same order```
  ...
end

fun check-doubling(recipe1 :: Table, recipe2 :: Table) -> Boolean:
  doc: ```Check that the second recipe doubles the amount of every
  ingredient in the first recipe```
  ...
end

fun check-ingredients-needed(plan :: Table, book :: List<Page>,
                             shopping :: Table) -> Boolean:
  doc: ```check that every ingredient in the shopping list is
  mentioned in one of the recipes that the plan uses```
  ...
end

To use these checks in testing the other functions on this assignment, you can write checks as follows:

check:
  scaled1 = scale-recipe(my-recipe, 2) # call the function
  check-doubling(my-recipe, scaled1)   # check the property
end

This calls the function that you want to test (scale-recipe), saves the result, and uses the checker to make sure the relationship between the input and output tables hold.

The shopping-list checker takes the meal plan, the recipe book, and the shopping list that was produced from them, so you can write:

check:
  book = load-recipe-book(recipe-names)
  shopping = shopping-list(meal-plan, book)
  check-ingredients-needed(meal-plan, book, shopping)
end

Task 7

Use your checkers to test the agent's shopping-list from Task 5. Write checks that run check-ingredients-needed on its output for meal-plan and for at least two meal plans of your own.

Part 7Reflecting on Working with an Agent

Task 8

In your worksheet, answer the following about generating shopping-list with Claude and reading what it produced:

  • How did working with Claude compare to your experience using Gemini for code generation? Consider the code it wrote, how it explained it, and anything else you noticed.
  • Which parts of reading the generated function felt easy? Which felt challenging? Be specific about the steps or expressions involved.

Part 8Stepping Back and Considering the Design

Task 9

A real-world version of this program would have to deal with issues that we simplified away for this homework. Identify three ways that the setup for this assignment was unrealistic and write them in your worksheet. For starters, consider the contents of the data that we started with. Don't focus on errors (like "a recipe might list an ingredient but have amount 0 for it"). Instead, imagine you were running a meal-plan company and trying to decide whether this program would cover real-world recipes and meal plans.

Part 9Copying Over your Check blocks

For the autograder to work, your hw5-tests.arr file should contain ONLY checks for the functions that we told you to write on the handout. Checks on your helper functions should not be copied over (they should stay in your hw5-code file).

Use your favorite AI agent/tool to copy the relevant parts of hw5-code.arr over to a new file hw5-tests.arr. Here's a prompt you can use (you'll also need to upload or paste in your hw5-code.arr file):

A prompt you can use
I have attached or included a Pyret file named hw5-code.arr. I need a new Pyret file
that contains only some of its tests from the included file, as follows.

Copy over every check block that tests any of these functions:
  normalize-recipe, load-recipe-book, find-recipe, scale-recipe,
  shopping-list, check-same-ingredients, check-doubling, check-ingredients-needed
A check block that also tests other functions should be copied, but with the
lines that test those other functions removed.

Do not copy checks that only test any other function, such as helper
functions. Do not copy any function definitions into the new
file, including the functions listed above.

If a copied check refers to a table, list, or other value that I defined in
hw5-code.arr (for example a test recipe or a test meal plan), copy that
definition into hw5-tests.arr too, above the checks that use it, so that
each check can run without my helper functions. Include only the definitions
that the copied checks need, and do not copy any definition that was given in
the starter code (such as meal-plan, units, or densities). If a definition
refers to one of my helper functions, tell me instead of copying it.

Do not change the text of any check, and do not add new tests.
When you are done, list every check you copied (by block name or its first
line) and every definition you included, and list any checks you skipped
because they test helper functions.

Testing and Grading

For testing, focus on core tests (skip the other kinds) for normalize-recipe, find-recipe, and scale-recipe. We won't be looking at your testing on other functions.

You should also use the property-based tests from part 6, so we can check that you know how to use and write such tests.

Handin

Hand in these files, together, in one submission:

  • hw5-code.arr
  • hw5-tests.arr

Submit to Gradescope. You may submit as many times as you like before the deadline; we grade the last submission.