{
  "id": "P179",
  "slug": "recipe-scaler",
  "key": "P179-recipe-scaler",
  "title": "Recipe scaler",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P179 - Recipe scaler\n\nA roast for four, scaled to more or fewer servings: the ingredients by the\nservings, and the time by something else.\n\n- `main.eml` - the menu, the recipe, the time for bigger joints, the salt,\n  and servings of your own\n- `recipe.eml` - the ingredients, the time for the 1 kg joint, what a\n  teaspoon of each salt holds, a cube root by Newton's method, and the\n  scaled time\n\nHow each part works:\n\n- The ingredients scale with the servings, rounded half-up to the gram.\n- As in the corpus case `the-roast-eight-times-the-size-took-four-times-the-time`,\n  the time goes with the thickness squared, the 2/3 power of the weight:\n  the 1 kg joint takes 30 minutes, 2 kg 48, 4 kg 76 and 8 kg 120 - four\n  times as long for eight times the size, not 240 minutes.\n- As in `the-same-spoon-held-twice-the-seasoning`, a teaspoon is a volume:\n  the recipe's 2 teaspoons of flake salt are 6 g, and the same spoon of\n  fine table salt holds 12 g, twice as salty. Scaled, the salt is given in\n  grams.\n\nWhat is checked: menu choices 1 to 5; 1 to 32 servings. An empty answer\ncancels.\n\nSessions: `sessions/basic.in` gives:\n- the recipe, the bigger joints and the salt;\n- 10, 1 and 4 servings.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- servings: a cancel, 0, 33, x and 2.5;\n- 32 and 3 servings.\n\nBuilt on the verified corpus cases `the-roast-eight-times-the-size-took-four-times-the-time`\n(a joint scaled to eight times the weight and cooked eight times as long)\nand `the-same-spoon-held-twice-the-seasoning` (two teaspoons of fine salt\nmeasured for a recipe written for flake salt).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P179 recipe scaler: a roast for four - the recipe, the time for bigger\n# joints, the salt by the spoon, and the recipe scaled to servings of your\n# own.\nimport recipe\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    str(n) => s\n    \"\" => out\n    0 => k\n    len(s) => i\n    while i > 0:\n        if k == 3:\n            \",\" + out => out\n            0 => k\n        s[i - 1] + out => out\n        k + 1 => k\n        i - 1 => i\n    return out\n\ndef scaled(grams, n):\n    # grams for four, scaled to n servings, rounded half-up\n    grams * n * 2 + recipe.serves() => y\n    return int((y - y % (2 * recipe.serves())) / (2 * recipe.serves()))\n\ndef kg(grams):\n    # grams as kilograms to one place, half-up\n    grams + 50 => y\n    int((y - y % 100) / 100) => t\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10) + \" kg\"\n\ndef show_recipe():\n    \"A roast for 4:\" ^0\n    for i in recipe.ingredients():\n        (\"  \" + pad(i[0], 12) + lpad(commas(i[1]) + \" g\", 8)) ^0\n    (\"  \" + pad(\"salt\", 12) + str(recipe.salt_tsp()) + \" teaspoons of flake salt\") ^0\n    (\"Roast the \" + kg(recipe.ingredients()[0][1]) + \" joint \" + str(recipe.minutes()) + \" minutes at 200 C.\") ^0\n\ndef show_joints():\n    \"Bigger joints of the same shape, and their roasting time:\" ^0\n    (\"  \" + pad(\"servings\", 10) + pad(\"joint\", 9) + lpad(\"times the time, scaled\", 24) + lpad(\"needed\", 9)) ^0\n    for n in [4, 8, 16, 32]:\n        (\"  \" + 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\n    (\"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\n\ndef show_salt():\n    recipe.salt_tsp() => t\n    t * recipe.grams_per_tsp(\"flake\") => g\n    t * recipe.grams_per_tsp(\"fine\") => f\n    (\"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\n    (\"The same \" + str(g) + \" g of fine salt is \" + str(int(g / recipe.grams_per_tsp(\"fine\"))) + \" teaspoon. Better: weigh the salt.\") ^0\n\ndef own():\n    plain(input(\"servings (1 to 32)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 32:\n        \"Type a whole number from 1 to 32.\" ^0\n        return\n    int(t) => n\n    (\"For \" + str(n) + \":\") ^0\n    for i in recipe.ingredients():\n        (\"  \" + pad(i[0], 12) + lpad(commas(scaled(i[1], n)) + \" g\", 8)) ^0\n    scaled(recipe.salt_tsp() * recipe.grams_per_tsp(\"flake\"), n) => salt\n    (\"  \" + pad(\"salt\", 12) + lpad(str(salt) + \" g\", 8) + \"  weighed; by the spoon that is flake or fine, not the same\") ^0\n    recipe.time_for(n) => m\n    (\"Roast the \" + commas(scaled(1000, n)) + \" g joint \" + str(m) + \" minutes.\") ^0\n    if n != recipe.serves():\n        \"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\n\n\"== Recipe scaler ==\" ^0\n\"A roast for four, scaled - the ingredients by the servings, and the time by something else.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_recipe()\n    elif choice == \"2\":\n        show_joints()\n    elif choice == \"3\":\n        show_salt()\n    elif choice == \"4\":\n        own()\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import recipe\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef digits(t):\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            return False\n    return len(t) > 0\n\ndef commas(n):\n    s = str(n)\n    out = \"\"\n    k = 0\n    i = len(s)\n    while i > 0:\n        if k == 3:\n            out = \",\" + out\n            k = 0\n        out = s[i - 1] + out\n        k = k + 1\n        i = i - 1\n    return out\n\ndef scaled(grams, n):\n    y = grams * n * 2 + recipe.serves()\n    return int((y - y % (2 * recipe.serves())) / (2 * recipe.serves()))\n\ndef kg(grams):\n    y = grams + 50\n    t = int((y - y % 100) / 100)\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10) + \" kg\"\n\ndef show_recipe():\n    print(\"A roast for 4:\")\n    for i in recipe.ingredients():\n        print(\"  \" + pad(i[0], 12) + lpad(commas(i[1]) + \" g\", 8))\n    print(\"  \" + pad(\"salt\", 12) + str(recipe.salt_tsp()) + \" teaspoons of flake salt\")\n    print(\"Roast the \" + kg(recipe.ingredients()[0][1]) + \" joint \" + str(recipe.minutes()) + \" minutes at 200 C.\")\n\ndef show_joints():\n    print(\"Bigger joints of the same shape, and their roasting time:\")\n    print(\"  \" + pad(\"servings\", 10) + pad(\"joint\", 9) + lpad(\"times the time, scaled\", 24) + lpad(\"needed\", 9))\n    for n in [4, 8, 16, 32]:\n        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))\n    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.\")\n\ndef show_salt():\n    t = recipe.salt_tsp()\n    g = t * recipe.grams_per_tsp(\"flake\")\n    f = t * recipe.grams_per_tsp(\"fine\")\n    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.\")\n    print(\"The same \" + str(g) + \" g of fine salt is \" + str(int(g / recipe.grams_per_tsp(\"fine\"))) + \" teaspoon. Better: weigh the salt.\")\n\ndef own():\n    t = plain(input(\"servings (1 to 32)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(t) > 2 or not digits(t) or int(t) < 1 or int(t) > 32:\n        print(\"Type a whole number from 1 to 32.\")\n        return\n    n = int(t)\n    print(\"For \" + str(n) + \":\")\n    for i in recipe.ingredients():\n        print(\"  \" + pad(i[0], 12) + lpad(commas(scaled(i[1], n)) + \" g\", 8))\n    salt = scaled(recipe.salt_tsp() * recipe.grams_per_tsp(\"flake\"), n)\n    print(\"  \" + pad(\"salt\", 12) + lpad(str(salt) + \" g\", 8) + \"  weighed; by the spoon that is flake or fine, not the same\")\n    m = recipe.time_for(n)\n    print(\"Roast the \" + commas(scaled(1000, n)) + \" g joint \" + str(m) + \" minutes.\")\n    if n != recipe.serves():\n        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.\")\n\nprint(\"== Recipe scaler ==\")\nprint(\"A roast for four, scaled - the ingredients by the servings, and the time by something else.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_recipe()\n    elif choice == \"2\":\n        show_joints()\n    elif choice == \"3\":\n        show_salt()\n    elif choice == \"4\":\n        own()\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "recipe.eml",
      "eml": "# P179 recipe scaler - a roast for four, scaled to more or fewer servings.\n# The ingredients scale with the servings. As in the corpus case\n# the-roast-eight-times-the-size-took-four-times-the-time, the cooking time\n# does not: heat has to reach the middle, and the time goes with the\n# thickness squared, the 2/3 power of the weight, so a joint eight times\n# the size takes four times as long. And as in\n# the-same-spoon-held-twice-the-seasoning, a teaspoon is a volume, and a\n# teaspoon of fine salt holds twice the salt of a teaspoon of flakes.\n\ndef serves():\n    return 4\n\ndef ingredients():\n    # [what, grams for four]\n    return [[\"beef joint\", 1000], [\"onions\", 300], [\"carrots\", 400], [\"stock\", 250]]\n\ndef minutes():\n    # cooking time for the 1 kg joint\n    return 30\n\ndef grams_per_tsp(salt):\n    # how much a level teaspoon holds\n    if salt == \"flake\":\n        return 3\n    return 6\n\ndef salt_tsp():\n    # the recipe's salt for four: 2 teaspoons, written for flake salt\n    return 2\n\ndef cube_root(x):\n    # x > 0, by Newton's method from a start above the root\n    1.0 => r\n    while r * r * r < x:\n        r * 2.0 => r\n    0 => k\n    while k < 60:\n        (2.0 * r + x / (r * r)) / 3.0 => r\n        k + 1 => k\n    return r\n\ndef time_for(n):\n    # whole minutes for the joint scaled to n servings: 30 x (n / 4)^(2/3)\n    n / serves() => ratio\n    cube_root(ratio * ratio) => f\n    return int(minutes() * f + 0.5)\n",
      "python": "def serves():\n    return 4\n\ndef ingredients():\n    return [[\"beef joint\", 1000], [\"onions\", 300], [\"carrots\", 400], [\"stock\", 250]]\n\ndef minutes():\n    return 30\n\ndef grams_per_tsp(salt):\n    if salt == \"flake\":\n        return 3\n    return 6\n\ndef salt_tsp():\n    return 2\n\ndef cube_root(x):\n    r = 1.0\n    while r * r * r < x:\n        r = r * 2.0\n    k = 0\n    while k < 60:\n        r = (2.0 * r + x / (r * r)) / 3.0\n        k = k + 1\n    return r\n\ndef time_for(n):\n    ratio = n / serves()\n    f = cube_root(ratio * ratio)\n    return int(minutes() * f + 0.5)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\n0\n4\n33\n4\nx\n4\n2.5\n4\n32\n4\n3\n5\n",
      "screen": "== Recipe scaler ==\nA roast for four, scaled - the ingredients by the servings, and the time by something else.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> \nCancelled.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 0\nType a whole number from 1 to 32.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 33\nType a whole number from 1 to 32.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> x\nType a whole number from 1 to 32.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 2.5\nType a whole number from 1 to 32.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 32\nFor 32:\n  beef joint   8,000 g\n  onions       2,400 g\n  carrots      3,200 g\n  stock        2,000 g\n  salt            48 g  weighed; by the spoon that is flake or fine, not the same\nRoast the 8,000 g joint 120 minutes.\nScaled 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.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 3\nFor 3:\n  beef joint     750 g\n  onions         225 g\n  carrots        300 g\n  stock          188 g\n  salt             5 g  weighed; by the spoon that is flake or fine, not the same\nRoast the 750 g joint 25 minutes.\nScaled 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.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n10\n4\n1\n4\n4\n5\n",
      "screen": "== Recipe scaler ==\nA roast for four, scaled - the ingredients by the servings, and the time by something else.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 1\nA roast for 4:\n  beef joint   1,000 g\n  onions         300 g\n  carrots        400 g\n  stock          250 g\n  salt        2 teaspoons of flake salt\nRoast the 1.0 kg joint 30 minutes at 200 C.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 2\nBigger joints of the same shape, and their roasting time:\n  servings  joint      times the time, scaled   needed\n  4         1.0 kg                     30 min   30 min\n  8         2.0 kg                     60 min   48 min\n  16        4.0 kg                    120 min   76 min\n  32        8.0 kg                    240 min  120 min\nA 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.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 3\nThe 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.\nThe same 6 g of fine salt is 1 teaspoon. Better: weigh the salt.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 10\nFor 10:\n  beef joint   2,500 g\n  onions         750 g\n  carrots      1,000 g\n  stock          625 g\n  salt            15 g  weighed; by the spoon that is flake or fine, not the same\nRoast the 2,500 g joint 55 minutes.\nScaled 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.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 1\nFor 1:\n  beef joint     250 g\n  onions          75 g\n  carrots        100 g\n  stock           63 g\n  salt             2 g  weighed; by the spoon that is flake or fine, not the same\nRoast the 250 g joint 12 minutes.\nScaled 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.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 4\nservings (1 to 32)> 4\nFor 4:\n  beef joint   1,000 g\n  onions         300 g\n  carrots        400 g\n  stock          250 g\n  salt             6 g  weighed; by the spoon that is flake or fine, not the same\nRoast the 1,000 g joint 30 minutes.\n\n1) the recipe  2) bigger joints  3) the salt  4) servings of your own  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-roast-eight-times-the-size-took-four-times-the-time",
      "caseId": "964-the-roast-eight-times-the-size-took-four-times-the-time",
      "title": "The roast eight times the size took four times the time"
    },
    {
      "slug": "the-same-spoon-held-twice-the-seasoning",
      "caseId": "966-the-same-spoon-held-twice-the-seasoning",
      "title": "The same spoon held twice the seasoning"
    }
  ],
  "updated": "2026-10-19"
}
