{
  "id": "P002",
  "slug": "unit-converter",
  "key": "P002-unit-converter",
  "title": "Unit converter",
  "summary": "Convert lengths, weights, temperatures and volumes from any unit to any other of the same kind, and back again, with every typed unit and value checked first - from a text menu.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P002 - Unit converter\n\nConverts lengths, weights, temperatures and volumes from any unit to any other\nunit of the same kind, and reverses the last conversion. A text menu; units are\ntyped by their symbol, and each kind lists its units before asking.\n\n- `main.eml` - the menu loop and the checks on what is typed\n- `units.eml` - the units: which kind each one measures, the factors that\n  convert length, weight and volume through the metre, the gram and the\n  millilitre, and the scale and offset that convert temperature through Celsius\n- `text.eml` - trimming, joining words, and showing a number to six\n  significant digits\n\nWhat is checked before anything is converted: a unit must exist and measure the\nkind chosen - typing `kg` while converting a length is refused, not converted;\na value must be a finite number; a length, weight or volume cannot be negative,\nand no temperature can be below absolute zero. \"Reverse last\" works from the\nexact result, not from the six digits shown for it.\n\nSessions: `sessions/basic.in` converts in all four kinds and reverses two\nconversions; `sessions/bad-input.in` asks to reverse before anything was\nconverted, types an unknown and an empty choice, units that do not exist, are\nempty or measure another kind, values that are empty, not numbers or not\nfinite, a negative length and temperatures below absolute zero.\n\nBuilt on the verified corpus cases `unit-temperature-converter` (Celsius and\nFahrenheit, both ways) and `dimensional-unit-guard` (a unit carries the\ndimension it measures, and a conversion across dimensions is refused rather\nthan computed).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P002 unit converter: lengths, weights, temperatures and volumes, from any\n# unit to any other unit of the same kind, and back again. What is typed is\n# checked before anything is converted: the unit must exist and measure the\n# kind chosen, and the value must be a finite number the quantity can have -\n# no negative length, nothing below absolute zero.\nimport units\nimport text\n\ndef ask_unit(prompt, kind):\n    # A unit of this kind, or None once the problem has been shown.\n    text.trim(input(prompt)) => u\n    units.kind_of(u) => k\n    if k == None:\n        if u == \"\":\n            \"Type a unit.\" ^0\n        else:\n            (\"Unknown unit: \" + u + \". \" + kind + \" units: \" + text.join(units.symbols(kind))) ^0\n        return None\n    if k != kind:\n        (u + \" is a \" + k + \" unit, not a \" + kind + \" unit.\") ^0\n        return None\n    return u\n\ndef ask_value(prompt, u, kind):\n    # A value a quantity in unit u can have, or None once the problem has\n    # been shown.\n    text.trim(input(prompt)) => typed\n    if typed == \"\":\n        \"Type a number.\" ^0\n        return None\n    try:\n        float(typed) => x\n    except ValueError:\n        (\"Not a number: \" + typed) ^0\n        return None\n    if not (x - x == 0):\n        (\"Not a finite number: \" + typed) ^0\n        return None\n    if x < units.lowest(u):\n        if kind == \"temperature\":\n            (text.num(x) + \" \" + u + \" is below absolute zero (\" + text.num(units.lowest(u)) + \" \" + u + \").\") ^0\n        else:\n            (\"A \" + kind + \" cannot be negative.\") ^0\n        return None\n    return x\n\ndef convert_once(kind, last):\n    # One conversion of this kind. Returns the conversion \"reverse last\"\n    # works from next: this one, or the previous one if nothing was converted.\n    (kind + \" units: \" + text.join(units.symbols(kind))) ^0\n    ask_unit(\"from> \", kind) => frm\n    if frm == None:\n        return last\n    ask_unit(\"to> \", kind) => to\n    if to == None:\n        return last\n    ask_value(\"value in \" + frm + \"> \", frm, kind) => x\n    if x == None:\n        return last\n    units.convert(x, frm, to) => y\n    (text.num(x) + \" \" + frm + \" = \" + text.num(y) + \" \" + to) ^0\n    return [x, frm, to, y]\n\nNone => last\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Unit converter ==\" ^0\n    \"1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\" or choice == \"2\" or choice == \"3\" or choice == \"4\":\n        convert_once(units.kinds[int(choice) - 1], last) => last\n    elif choice == \"5\":\n        if last == None:\n            \"Nothing to reverse yet.\" ^0\n        else:\n            # From the exact result, not the six digits shown for it.\n            units.convert(last[3], last[2], last[1]) => back\n            (text.num(last[3]) + \" \" + last[2] + \" = \" + text.num(back) + \" \" + last[1]) ^0\n            [last[3], last[2], last[1], back] => last\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import units\nimport text\n\ndef ask_unit(prompt, kind):\n    u = text.trim(input(prompt))\n    k = units.kind_of(u)\n    if k == None:\n        if u == \"\":\n            print(\"Type a unit.\")\n        else:\n            print(\"Unknown unit: \" + u + \". \" + kind + \" units: \" + text.join(units.symbols(kind)))\n        return None\n    if k != kind:\n        print(u + \" is a \" + k + \" unit, not a \" + kind + \" unit.\")\n        return None\n    return u\n\ndef ask_value(prompt, u, kind):\n    typed = text.trim(input(prompt))\n    if typed == \"\":\n        print(\"Type a number.\")\n        return None\n    try:\n        x = float(typed)\n    except ValueError:\n        print(\"Not a number: \" + typed)\n        return None\n    if not x - x == 0:\n        print(\"Not a finite number: \" + typed)\n        return None\n    if x < units.lowest(u):\n        if kind == \"temperature\":\n            print(text.num(x) + \" \" + u + \" is below absolute zero (\" + text.num(units.lowest(u)) + \" \" + u + \").\")\n        else:\n            print(\"A \" + kind + \" cannot be negative.\")\n        return None\n    return x\n\ndef convert_once(kind, last):\n    print(kind + \" units: \" + text.join(units.symbols(kind)))\n    frm = ask_unit(\"from> \", kind)\n    if frm == None:\n        return last\n    to = ask_unit(\"to> \", kind)\n    if to == None:\n        return last\n    x = ask_value(\"value in \" + frm + \"> \", frm, kind)\n    if x == None:\n        return last\n    y = units.convert(x, frm, to)\n    print(text.num(x) + \" \" + frm + \" = \" + text.num(y) + \" \" + to)\n    return [x, frm, to, y]\n\nlast = None\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Unit converter ==\")\n    print(\"1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\" or choice == \"2\" or choice == \"3\" or choice == \"4\":\n        last = convert_once(units.kinds[int(choice) - 1], last)\n    elif choice == \"5\":\n        if last == None:\n            print(\"Nothing to reverse yet.\")\n        else:\n            back = units.convert(last[3], last[2], last[1])\n            print(text.num(last[3]) + \" \" + last[2] + \" = \" + text.num(back) + \" \" + last[1])\n            last = [last[3], last[2], last[1], back]\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "units.eml",
      "eml": "# P002 unit converter - the units. Length, weight and volume convert through\n# a base unit by a factor: the metre, the gram and the millilitre.\n# Temperature cannot: its scales start from different zeros, so it converts\n# through Celsius with an offset as well as a scale. The factors are the\n# exact definitions (1 in = 2.54 cm, 1 lb = 453.59237 g, 1 US gallon =\n# 3785.411784 ml), not rounded values.\n\n[\"length\", \"weight\", \"temperature\", \"volume\"] => kinds\n\n{\n    \"length\": [\"mm\", \"cm\", \"m\", \"km\", \"in\", \"ft\", \"yd\", \"mi\"],\n    \"weight\": [\"mg\", \"g\", \"kg\", \"t\", \"oz\", \"lb\"],\n    \"temperature\": [\"C\", \"F\", \"K\"],\n    \"volume\": [\"ml\", \"l\", \"m3\", \"tsp\", \"tbsp\", \"floz\", \"cup\", \"pt\", \"qt\", \"gal\"],\n} => unit_lists\n\n# How many base units one unit is. The volumes from tsp on are US customary.\n{\n    \"mm\": 0.001,\n    \"cm\": 0.01,\n    \"m\": 1.0,\n    \"km\": 1000.0,\n    \"in\": 0.0254,\n    \"ft\": 0.3048,\n    \"yd\": 0.9144,\n    \"mi\": 1609.344,\n    \"mg\": 0.001,\n    \"g\": 1.0,\n    \"kg\": 1000.0,\n    \"t\": 1000000.0,\n    \"oz\": 28.349523125,\n    \"lb\": 453.59237,\n    \"ml\": 1.0,\n    \"l\": 1000.0,\n    \"m3\": 1000000.0,\n    \"tsp\": 4.92892159375,\n    \"tbsp\": 14.78676478125,\n    \"floz\": 29.5735295625,\n    \"cup\": 236.5882365,\n    \"pt\": 473.176473,\n    \"qt\": 946.352946,\n    \"gal\": 3785.411784,\n} => factors\n\ndef kind_of(u):\n    # The kind a unit measures, or None for a unit this converter does not know.\n    for k in kinds:\n        if u in unit_lists[k]:\n            return k\n    return None\n\ndef symbols(kind):\n    return unit_lists[kind]\n\ndef to_celsius(x, u):\n    if u == \"F\":\n        return (x - 32) * 5 / 9\n    if u == \"K\":\n        return x - 273.15\n    return x\n\ndef from_celsius(c, u):\n    if u == \"F\":\n        return c * 9 / 5 + 32\n    if u == \"K\":\n        return c + 273.15\n    return c\n\ndef convert(x, frm, to):\n    # x in unit frm, expressed in unit to. Both units measure the same kind:\n    # the menu checks that before it asks for a value.\n    if kind_of(frm) == \"temperature\":\n        return from_celsius(to_celsius(x, frm), to)\n    return x * factors[frm] / factors[to]\n\ndef lowest(u):\n    # The smallest value a quantity in unit u can have: absolute zero for a\n    # temperature, nothing below zero for everything else.\n    if u == \"C\":\n        return -273.15\n    if u == \"F\":\n        return -459.67\n    return 0.0\n",
      "python": "kinds = [\"length\", \"weight\", \"temperature\", \"volume\"]\nunit_lists = {\"length\": [\"mm\", \"cm\", \"m\", \"km\", \"in\", \"ft\", \"yd\", \"mi\"], \"weight\": [\"mg\", \"g\", \"kg\", \"t\", \"oz\", \"lb\"], \"temperature\": [\"C\", \"F\", \"K\"], \"volume\": [\"ml\", \"l\", \"m3\", \"tsp\", \"tbsp\", \"floz\", \"cup\", \"pt\", \"qt\", \"gal\"]}\nfactors = {\"mm\": 0.001, \"cm\": 0.01, \"m\": 1.0, \"km\": 1000.0, \"in\": 0.0254, \"ft\": 0.3048, \"yd\": 0.9144, \"mi\": 1609.344, \"mg\": 0.001, \"g\": 1.0, \"kg\": 1000.0, \"t\": 1000000.0, \"oz\": 28.349523125, \"lb\": 453.59237, \"ml\": 1.0, \"l\": 1000.0, \"m3\": 1000000.0, \"tsp\": 4.92892159375, \"tbsp\": 14.78676478125, \"floz\": 29.5735295625, \"cup\": 236.5882365, \"pt\": 473.176473, \"qt\": 946.352946, \"gal\": 3785.411784}\n\ndef kind_of(u):\n    for k in kinds:\n        if u in unit_lists[k]:\n            return k\n    return None\n\ndef symbols(kind):\n    return unit_lists[kind]\n\ndef to_celsius(x, u):\n    if u == \"F\":\n        return (x - 32) * 5 / 9\n    if u == \"K\":\n        return x - 273.15\n    return x\n\ndef from_celsius(c, u):\n    if u == \"F\":\n        return c * 9 / 5 + 32\n    if u == \"K\":\n        return c + 273.15\n    return c\n\ndef convert(x, frm, to):\n    if kind_of(frm) == \"temperature\":\n        return from_celsius(to_celsius(x, frm), to)\n    return x * factors[frm] / factors[to]\n\ndef lowest(u):\n    if u == \"C\":\n        return -273.15\n    if u == \"F\":\n        return -459.67\n    return 0.0\n"
    },
    {
      "name": "text.eml",
      "eml": "# P002 unit converter - text helpers. The interpreter that checks every\n# session does not run string methods yet, so trimming and joining are\n# written out here.\n\ndef trim(s):\n    # s without the spaces at either end.\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 join(words):\n    # The words with one space between each two.\n    \"\" => out\n    for w in words:\n        if out != \"\":\n            out + \" \" => out\n        out + w => out\n    return out\n\ndef num(x):\n    # Six significant digits, as a calculator shows them. Adding 0.0 turns\n    # -0.0 into 0.0, so a typed \"-0\" never comes back as \"-0\".\n    return \"%g\" % (x + 0.0)\n",
      "python": "def trim(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 join(words):\n    out = \"\"\n    for w in words:\n        if out != \"\":\n            out = out + \" \"\n        out = out + w\n    return out\n\ndef num(x):\n    return \"%g\" % (x + 0.0)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "5\n9\n\n 1\nparsec\n1\n\n1\nkg\n1\n  km\nmi\nabc\n1\nkm\nm\n\n1\nkm\nm\n-3\n3\nF\nC\n-500\n3\nK\nC\n-0.5\n2\ng\noz\nnan\n2\ng\noz\ninf\n4\nl\nl\n-0\n5\n6\n",
      "screen": "\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 5\nNothing to reverse yet.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 9\nPick a number from 1 to 6.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> \nPick a number from 1 to 6.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice>  1\nlength units: mm cm m km in ft yd mi\nfrom> parsec\nUnknown unit: parsec. length units: mm cm m km in ft yd mi\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> \nType a unit.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> kg\nkg is a weight unit, not a length unit.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom>   km\nto> mi\nvalue in km> abc\nNot a number: abc\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> km\nto> m\nvalue in km> \nType a number.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> km\nto> m\nvalue in km> -3\nA length cannot be negative.\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 3\ntemperature units: C F K\nfrom> F\nto> C\nvalue in F> -500\n-500 F is below absolute zero (-459.67 F).\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 3\ntemperature units: C F K\nfrom> K\nto> C\nvalue in K> -0.5\n-0.5 K is below absolute zero (0 K).\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 2\nweight units: mg g kg t oz lb\nfrom> g\nto> oz\nvalue in g> nan\nNot a finite number: nan\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 2\nweight units: mg g kg t oz lb\nfrom> g\nto> oz\nvalue in g> inf\nNot a finite number: inf\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 4\nvolume units: ml l m3 tsp tbsp floz cup pt qt gal\nfrom> l\nto> l\nvalue in l> -0\n0 l = 0 l\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 5\n0 l = 0 l\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nkm\nmi\n10\n5\n2\nlb\nkg\n150\n3\nF\nC\n98.6\n3\nC\nF\n100\n3\nK\nC\n0\n4\ngal\nl\n2\n4\ncup\nml\n1\n5\n1\nft\ncm\n6\n6\n",
      "screen": "\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> km\nto> mi\nvalue in km> 10\n10 km = 6.21371 mi\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 5\n6.21371 mi = 10 km\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 2\nweight units: mg g kg t oz lb\nfrom> lb\nto> kg\nvalue in lb> 150\n150 lb = 68.0389 kg\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 3\ntemperature units: C F K\nfrom> F\nto> C\nvalue in F> 98.6\n98.6 F = 37 C\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 3\ntemperature units: C F K\nfrom> C\nto> F\nvalue in C> 100\n100 C = 212 F\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 3\ntemperature units: C F K\nfrom> K\nto> C\nvalue in K> 0\n0 K = -273.15 C\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 4\nvolume units: ml l m3 tsp tbsp floz cup pt qt gal\nfrom> gal\nto> l\nvalue in gal> 2\n2 gal = 7.57082 l\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 4\nvolume units: ml l m3 tsp tbsp floz cup pt qt gal\nfrom> cup\nto> ml\nvalue in cup> 1\n1 cup = 236.588 ml\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 5\n236.588 ml = 1 cup\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 1\nlength units: mm cm m km in ft yd mi\nfrom> ft\nto> cm\nvalue in ft> 6\n6 ft = 182.88 cm\n\n== Unit converter ==\n1) length  2) weight  3) temperature  4) volume  5) reverse last  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "unit-temperature-converter",
      "caseId": "009-unit-temperature-converter",
      "title": "Case corpus: a self-authored unit-conversion program"
    },
    {
      "slug": "dimensional-unit-guard",
      "caseId": "202-dimensional-unit-guard",
      "title": "A factor table will convert metres to seconds"
    }
  ],
  "updated": "2026-09-27"
}
