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),roundandisVolumeare ready to use. You finishtoMilliliters.scale.js: the scaler. You write its import line, theServingsErrorclass,checkServings,scaleIngredientsandtoMetric.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.jsfile 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:
- Problem 0: at the top of
scale.js, importround,isVolumeandtoMillilitersfrom"./units.js". - Problem 1: finish
toMillilitersinunits.js. A unit it doesn't know, such as"pinch", throws anErrorwhose message names the unit. - Problem 2: finish the
ServingsErrorclass, so itsnameis"ServingsError"and it keeps its message. - Problems 3 to 5: finish
checkServings, thenscaleIngredients(which uses it), thentoMetric. The last two return new lists and leave the one they're given as it is. - Problem 6: at the bottom of
main.js, scale the pancakes to"ten"servings inside atry, and in thecatch, print the error's name and message as two lines,6) name: ...and7) 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.
toMillilitersthrows a plainError;checkServingsthrows your ownServingsError. Throwing Your Own Errors shows both, and thesuper(message)line a custom error needs. - Use what's ready.
isVolume(unit)tells you whether a unit can be converted, andround(amount)rounds to 2 decimal places. They're inunits.js, soscale.jsimports 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.