<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P179-recipe-scaler/ | updated: 2026-10-19 -->

# P179 Recipe scaler

A roast for four scaled to more or fewer servings: the ingredients scale with them, the time with the 2/3 power of the weight, and a teaspoon of fine salt holds twice a teaspoon of flakes.

EML-P project `projects/recipe-scaler` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/recipe-scaler` runs it and `eml project verify projects/recipe-scaler` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-roast-eight-times-the-size-took-four-times-the-time (https://efficientnewlanguage.org/cases/964-the-roast-eight-times-the-size-took-four-times-the-time/), the-same-spoon-held-twice-the-seasoning (https://efficientnewlanguage.org/cases/966-the-same-spoon-held-twice-the-seasoning/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
4

4
0
4
33
4
x
4
2.5
4
32
4
3
5
```

Screen:

```text
== Recipe scaler ==
A roast for four, scaled - the ingredients by the servings, and the time by something else.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 0
Pick a number from 1 to 5.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> x
Pick a number from 1 to 5.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 
Cancelled.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 0
Type a whole number from 1 to 32.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 33
Type a whole number from 1 to 32.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> x
Type a whole number from 1 to 32.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 2.5
Type a whole number from 1 to 32.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 32
For 32:
  beef joint   8,000 g
  onions       2,400 g
  carrots      3,200 g
  stock        2,000 g
  salt            48 g  weighed; by the spoon that is flake or fine, not the same
Roast the 8,000 g joint 120 minutes.
Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 3
For 3:
  beef joint     750 g
  onions         225 g
  carrots        300 g
  stock          188 g
  salt             5 g  weighed; by the spoon that is flake or fine, not the same
Roast the 750 g joint 25 minutes.
Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
4
10
4
1
4
4
5
```

Screen:

```text
== Recipe scaler ==
A roast for four, scaled - the ingredients by the servings, and the time by something else.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 1
A roast for 4:
  beef joint   1,000 g
  onions         300 g
  carrots        400 g
  stock          250 g
  salt        2 teaspoons of flake salt
Roast the 1.0 kg joint 30 minutes at 200 C.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 2
Bigger joints of the same shape, and their roasting time:
  servings  joint      times the time, scaled   needed
  4         1.0 kg                     30 min   30 min
  8         2.0 kg                     60 min   48 min
  16        4.0 kg                    120 min   76 min
  32        8.0 kg                    240 min  120 min
A joint eight times the size takes 120 minutes, four times as long, not 240: the heat has to reach the middle, and that time goes with the thickness squared, the 2/3 power of the weight.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 3
The recipe's 2 teaspoons are written for flake salt: 6 g. Measured with the same spoon, fine table salt is 12 g, twice as salty: a teaspoon is a volume, and fine grains pack it.
The same 6 g of fine salt is 1 teaspoon. Better: weigh the salt.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 10
For 10:
  beef joint   2,500 g
  onions         750 g
  carrots      1,000 g
  stock          625 g
  salt            15 g  weighed; by the spoon that is flake or fine, not the same
Roast the 2,500 g joint 55 minutes.
Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 1
For 1:
  beef joint     250 g
  onions          75 g
  carrots        100 g
  stock           63 g
  salt             2 g  weighed; by the spoon that is flake or fine, not the same
Roast the 250 g joint 12 minutes.
Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 4
servings (1 to 32)> 4
For 4:
  beef joint   1,000 g
  onions         300 g
  carrots        400 g
  stock          250 g
  salt             6 g  weighed; by the spoon that is flake or fine, not the same
Roast the 1,000 g joint 30 minutes.

1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P179 recipe scaler: a roast for four - the recipe, the time for bigger
# joints, the salt by the spoon, and the recipe scaled to servings of your
# own.
import recipe

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    str(n) => s
    "" => out
    0 => k
    len(s) => i
    while i > 0:
        if k == 3:
            "," + out => out
            0 => k
        s[i - 1] + out => out
        k + 1 => k
        i - 1 => i
    return out

def scaled(grams, n):
    # grams for four, scaled to n servings, rounded half-up
    grams * n * 2 + recipe.serves() => y
    return int((y - y % (2 * recipe.serves())) / (2 * recipe.serves()))

def kg(grams):
    # grams as kilograms to one place, half-up
    grams + 50 => y
    int((y - y % 100) / 100) => t
    return str(int((t - t % 10) / 10)) + "." + str(t % 10) + " kg"

def show_recipe():
    "A roast for 4:" ^0
    for i in recipe.ingredients():
        ("  " + pad(i[0], 12) + lpad(commas(i[1]) + " g", 8)) ^0
    ("  " + pad("salt", 12) + str(recipe.salt_tsp()) + " teaspoons of flake salt") ^0
    ("Roast the " + kg(recipe.ingredients()[0][1]) + " joint " + str(recipe.minutes()) + " minutes at 200 C.") ^0

def show_joints():
    "Bigger joints of the same shape, and their roasting time:" ^0
    ("  " + pad("servings", 10) + pad("joint", 9) + lpad("times the time, scaled", 24) + lpad("needed", 9)) ^0
    for n in [4, 8, 16, 32]:
        ("  " + pad(str(n), 10) + pad(kg(scaled(1000, n)), 9) + lpad(str(int(recipe.minutes() * n / recipe.serves())) + " min", 24) + lpad(str(recipe.time_for(n)) + " min", 9)) ^0
    ("A joint eight times the size takes " + str(recipe.time_for(32)) + " minutes, four times as long, not " + str(8 * recipe.minutes()) + ": the heat has to reach the middle, and that time goes with the thickness squared, the 2/3 power of the weight.") ^0

def show_salt():
    recipe.salt_tsp() => t
    t * recipe.grams_per_tsp("flake") => g
    t * recipe.grams_per_tsp("fine") => f
    ("The recipe's " + str(t) + " teaspoons are written for flake salt: " + str(g) + " g. Measured with the same spoon, fine table salt is " + str(f) + " g, twice as salty: a teaspoon is a volume, and fine grains pack it.") ^0
    ("The same " + str(g) + " g of fine salt is " + str(int(g / recipe.grams_per_tsp("fine"))) + " teaspoon. Better: weigh the salt.") ^0

def own():
    plain(input("servings (1 to 32)> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 32:
        "Type a whole number from 1 to 32." ^0
        return
    int(t) => n
    ("For " + str(n) + ":") ^0
    for i in recipe.ingredients():
        ("  " + pad(i[0], 12) + lpad(commas(scaled(i[1], n)) + " g", 8)) ^0
    scaled(recipe.salt_tsp() * recipe.grams_per_tsp("flake"), n) => salt
    ("  " + pad("salt", 12) + lpad(str(salt) + " g", 8) + "  weighed; by the spoon that is flake or fine, not the same") ^0
    recipe.time_for(n) => m
    ("Roast the " + commas(scaled(1000, n)) + " g joint " + str(m) + " minutes.") ^0
    if n != recipe.serves():
        "Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller." ^0

"== Recipe scaler ==" ^0
"A roast for four, scaled - the ingredients by the servings, and the time by something else." ^0
True => running
while running:
    "" ^0
    "1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_recipe()
    elif choice == "2":
        show_joints()
    elif choice == "3":
        show_salt()
    elif choice == "4":
        own()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import recipe

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def digits(t):
    for c in t:
        if c < "0" or c > "9":
            return False
    return len(t) > 0

def commas(n):
    s = str(n)
    out = ""
    k = 0
    i = len(s)
    while i > 0:
        if k == 3:
            out = "," + out
            k = 0
        out = s[i - 1] + out
        k = k + 1
        i = i - 1
    return out

def scaled(grams, n):
    y = grams * n * 2 + recipe.serves()
    return int((y - y % (2 * recipe.serves())) / (2 * recipe.serves()))

def kg(grams):
    y = grams + 50
    t = int((y - y % 100) / 100)
    return str(int((t - t % 10) / 10)) + "." + str(t % 10) + " kg"

def show_recipe():
    print("A roast for 4:")
    for i in recipe.ingredients():
        print("  " + pad(i[0], 12) + lpad(commas(i[1]) + " g", 8))
    print("  " + pad("salt", 12) + str(recipe.salt_tsp()) + " teaspoons of flake salt")
    print("Roast the " + kg(recipe.ingredients()[0][1]) + " joint " + str(recipe.minutes()) + " minutes at 200 C.")

def show_joints():
    print("Bigger joints of the same shape, and their roasting time:")
    print("  " + pad("servings", 10) + pad("joint", 9) + lpad("times the time, scaled", 24) + lpad("needed", 9))
    for n in [4, 8, 16, 32]:
        print("  " + pad(str(n), 10) + pad(kg(scaled(1000, n)), 9) + lpad(str(int(recipe.minutes() * n / recipe.serves())) + " min", 24) + lpad(str(recipe.time_for(n)) + " min", 9))
    print("A joint eight times the size takes " + str(recipe.time_for(32)) + " minutes, four times as long, not " + str(8 * recipe.minutes()) + ": the heat has to reach the middle, and that time goes with the thickness squared, the 2/3 power of the weight.")

def show_salt():
    t = recipe.salt_tsp()
    g = t * recipe.grams_per_tsp("flake")
    f = t * recipe.grams_per_tsp("fine")
    print("The recipe's " + str(t) + " teaspoons are written for flake salt: " + str(g) + " g. Measured with the same spoon, fine table salt is " + str(f) + " g, twice as salty: a teaspoon is a volume, and fine grains pack it.")
    print("The same " + str(g) + " g of fine salt is " + str(int(g / recipe.grams_per_tsp("fine"))) + " teaspoon. Better: weigh the salt.")

def own():
    t = plain(input("servings (1 to 32)> "))
    if t == "":
        print("Cancelled.")
        return
    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 32:
        print("Type a whole number from 1 to 32.")
        return
    n = int(t)
    print("For " + str(n) + ":")
    for i in recipe.ingredients():
        print("  " + pad(i[0], 12) + lpad(commas(scaled(i[1], n)) + " g", 8))
    salt = scaled(recipe.salt_tsp() * recipe.grams_per_tsp("flake"), n)
    print("  " + pad("salt", 12) + lpad(str(salt) + " g", 8) + "  weighed; by the spoon that is flake or fine, not the same")
    m = recipe.time_for(n)
    print("Roast the " + commas(scaled(1000, n)) + " g joint " + str(m) + " minutes.")
    if n != recipe.serves():
        print("Scaled like the ingredients, the time would be off: it grows more slowly than the weight when the joint is bigger, and falls more slowly when it is smaller.")

print("== Recipe scaler ==")
print("A roast for four, scaled - the ingredients by the servings, and the time by something else.")
running = True
while running:
    print("")
    print("1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_recipe()
    elif choice == "2":
        show_joints()
    elif choice == "3":
        show_salt()
    elif choice == "4":
        own()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### recipe.eml

```eml
# P179 recipe scaler - a roast for four, scaled to more or fewer servings.
# The ingredients scale with the servings. As in the corpus case
# the-roast-eight-times-the-size-took-four-times-the-time, the cooking time
# does not: heat has to reach the middle, and the time goes with the
# thickness squared, the 2/3 power of the weight, so a joint eight times
# the size takes four times as long. And as in
# the-same-spoon-held-twice-the-seasoning, a teaspoon is a volume, and a
# teaspoon of fine salt holds twice the salt of a teaspoon of flakes.

def serves():
    return 4

def ingredients():
    # [what, grams for four]
    return [["beef joint", 1000], ["onions", 300], ["carrots", 400], ["stock", 250]]

def minutes():
    # cooking time for the 1 kg joint
    return 30

def grams_per_tsp(salt):
    # how much a level teaspoon holds
    if salt == "flake":
        return 3
    return 6

def salt_tsp():
    # the recipe's salt for four: 2 teaspoons, written for flake salt
    return 2

def cube_root(x):
    # x > 0, by Newton's method from a start above the root
    1.0 => r
    while r * r * r < x:
        r * 2.0 => r
    0 => k
    while k < 60:
        (2.0 * r + x / (r * r)) / 3.0 => r
        k + 1 => k
    return r

def time_for(n):
    # whole minutes for the joint scaled to n servings: 30 x (n / 4)^(2/3)
    n / serves() => ratio
    cube_root(ratio * ratio) => f
    return int(minutes() * f + 0.5)
```

Python projection of recipe.eml:

```python
def serves():
    return 4

def ingredients():
    return [["beef joint", 1000], ["onions", 300], ["carrots", 400], ["stock", 250]]

def minutes():
    return 30

def grams_per_tsp(salt):
    if salt == "flake":
        return 3
    return 6

def salt_tsp():
    return 2

def cube_root(x):
    r = 1.0
    while r * r * r < x:
        r = r * 2.0
    k = 0
    while k < 60:
        r = (2.0 * r + x / (r * r)) / 3.0
        k = k + 1
    return r

def time_for(n):
    ratio = n / serves()
    f = cube_root(ratio * ratio)
    return int(minutes() * f + 0.5)
```

## README

# P179 - Recipe scaler

A roast for four, scaled to more or fewer servings: the ingredients by the
servings, and the time by something else.

- `main.eml` - the menu, the recipe, the time for bigger joints, the salt,
  and servings of your own
- `recipe.eml` - the ingredients, the time for the 1 kg joint, what a
  teaspoon of each salt holds, a cube root by Newton's method, and the
  scaled time

How each part works:

- The ingredients scale with the servings, rounded half-up to the gram.
- As in the corpus case `the-roast-eight-times-the-size-took-four-times-the-time`,
  the time goes with the thickness squared, the 2/3 power of the weight:
  the 1 kg joint takes 30 minutes, 2 kg 48, 4 kg 76 and 8 kg 120 - four
  times as long for eight times the size, not 240 minutes.
- As in `the-same-spoon-held-twice-the-seasoning`, a teaspoon is a volume:
  the recipe's 2 teaspoons of flake salt are 6 g, and the same spoon of
  fine table salt holds 12 g, twice as salty. Scaled, the salt is given in
  grams.

What is checked: menu choices 1 to 5; 1 to 32 servings. An empty answer
cancels.

Sessions: `sessions/basic.in` gives:
- the recipe, the bigger joints and the salt;
- 10, 1 and 4 servings.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- servings: a cancel, 0, 33, x and 2.5;
- 32 and 3 servings.

Built on the verified corpus cases `the-roast-eight-times-the-size-took-four-times-the-time`
(a joint scaled to eight times the weight and cooked eight times as long)
and `the-same-spoon-held-twice-the-seasoning` (two teaspoons of fine salt
measured for a recipe written for flake salt).
