Exercise: Recipe Scaler

Finish a recipe scaler split into modules, which converts cups to milliliters, scales a recipe from 4 servings to 10 and refuses bad servings with a ServingsError, and let the tests check each piece and each error.

Journeyman · JavaScript · Software Development · Updated

The Saturday Pancakes card serves 4, and you're making brunch for 10. Your friend who's coming from overseas cooks in milliliters, not cups. And someone will, sooner or later, type -2 or ten into the servings box. In this exercise you finish a small recipe scaler that handles all three: it's split into modules, it converts units, and it refuses servings it can't use with an error of its own. Tests check every function, and every error.

Get the exercise code

In the Exercise box beside the lesson, choose Get the starter on GitHub to open its GitHub repository, exercise-javascript-recipe-scaler. Click Code, then Download ZIP, and unzip it. Or clone it with Git. Then open the folder in VS Code.

Each file has one job:

  • units.js: kitchen measurements. ML_PER_UNIT (240 ml in a cup, 15 in a tablespoon, 5 in a teaspoon), round and isVolume are ready to use. You finish toMilliliters.
  • scale.js: the scaler. You write its import line, the ServingsError class, checkServings, scaleIngredients and toMetric.
  • main.js: prints the pancake card scaled for 10. Ready to use, except the last problem.
  • recipe.js: the recipe card itself, as a default export. Ready to use.
  • units.test.js, scale.test.js, main.test.js: the tests. You don't need to change these.
  • package.json: says "type": "module", so every .js file here is a module.

What to do

Each function you finish is already there, with its parameters, and returns a placeholder for now. The comment above it says what it should do, with examples. Here's one, from scale.js:

/*
 * 3. Check a number of servings. Return it when it's a whole number above 0.
 *    Otherwise throw a ServingsError whose message says what's wrong and shows the value that came in.
 *
 *    checkServings(10)      -> 10
 *    checkServings(-2)      -> throws ServingsError: Servings must be a whole number above 0, but got -2.
 *    checkServings(2.5)     -> throws ServingsError (half a serving isn't a serving)
 *    checkServings("ten")   -> throws ServingsError: Servings must be a whole number above 0, but got ten.
 */
export function checkServings(servings) {
  return servings;
}

Work through the problems in order:

  1. Problem 0: at the top of scale.js, import round, isVolume and toMilliliters from "./units.js".
  2. Problem 1: finish toMilliliters in units.js. A unit it doesn't know, such as "pinch", throws an Error whose message names the unit.
  3. Problem 2: finish the ServingsError class, so its name is "ServingsError" and it keeps its message.
  4. Problems 3 to 5: finish checkServings, then scaleIngredients (which uses it), then toMetric. The last two return new lists and leave the one they're given as it is.
  5. Problem 6: at the bottom of main.js, scale the pancakes to "ten" servings inside a try, and in the catch, print the error's name and message as two lines, 6) name: ... and 7) message: .... The program keeps going after it.

Check your work

In VS Code's terminal (Terminal > New Terminal), from the exercise folder, run the tests:

node --test

At first most tests fail. That's expected: each problem you finish turns its tests green. When a test fails, its message says what it tried and what it expected, for example checkServings(-2) should throw a ServingsError, but it didn't throw anything.

To see the program itself, run:

node main.js

Hints

  • Work out one example by hand. From 4 servings to 10, every amount is multiplied by the same number. What is it?
  • Two kinds of error, two lessons. toMilliliters throws a plain Error; checkServings throws your own ServingsError. Throwing Your Own Errors shows both, and the super(message) line a custom error needs.
  • Use what's ready. isVolume(unit) tells you whether a unit can be converted, and round(amount) rounds to 2 decimal places. They're in units.js, so scale.js imports them, as in Modules: import and export.
  • A new list, not a changed one. If a test says the list it gave you was changed, look for a line that sets item.amount = .... Build a new object for each ingredient instead, as in Spread and Rest.
  • Lost? Set a breakpoint inside the function you're working on, start the debugger on main.js, and step through it while you watch the values, as in Debugging with Breakpoints.

When you're done

Every test is green, and node main.js prints:

Saturday Pancakes, for 10 (the card serves 4)
  flour: 5 cups
  milk: 3.75 cups
  eggs: 5 whole
  sugar: 5 tbsp
  baking powder: 5 tsp
  salt: 1.25 tsp
The same, in milliliters
  flour: 1200 ml
  milk: 900 ml
  eggs: 5 whole
  sugar: 75 ml
  baking powder: 25 ml
  salt: 6.25 ml
6) name: ServingsError
7) message: Servings must be a whole number above 0, but got ten.
Enjoy your brunch!

Now look back at your error messages. If a friend typed 0 into the servings box and saw your message, would they know what to type instead?

So what

Splitting a program into modules, checking every value at the door and throwing errors that say what to fix: that's how real apps stay working when people use them in ways nobody planned.

Key terms

Related