{
  "id": "P021",
  "slug": "base-converter",
  "key": "P021-base-converter",
  "title": "Base converter",
  "summary": "Convert whole numbers of any length between bases 2 to 36 by long division on their digits, show a number in bases 2, 8, 10 and 16 at once, and turn numbers into Roman numerals and back - refusing a numeral that is not written the standard way and saying how it should be - from a text menu.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P021 - Base converter\n\nConverts whole numbers between any two bases from 2 to 36, shows a number in\nbases 2, 8, 10 and 16 at once, and turns numbers into Roman numerals and\nback. A text menu.\n\n- `main.eml` - the menu and its questions, with their checks\n- `bases.eml` - digits and long division by hand\n- `roman.eml` - writing and reading Roman numerals, and the standard-form\n  check\n- `text.eml` - trimming, capitals and whole numbers\n\nA number is kept as its list of digit values, most significant first, and\nconverted by long division done by hand: divide the whole list by the new\nbase, keep the remainder as the next digit, and repeat until nothing is left.\nEvery step uses small numbers only, so a number of up to 60 digits comes out\nexact - 123456789012345678901234567890 is BYW97UM9S91DLZ68TSI in base 36.\nDigits are 0-9 and then A-Z for 10 to 35, in either case; a minus sign in\nfront is kept.\n\nRoman numerals run from I to MMMCMXCIX (1 to 3999). Writing one walks a table\nfrom the largest value down, with the subtractive pairs (CM, CD, XC, XL, IX,\nIV) as entries of the table. Reading one adds each symbol's value, except\nthat a symbol smaller than the one after it is subtracted. That rule alone\nwould also accept IIII, IC or VV, so a numeral counts only if writing its\nvalue again gives exactly the same letters - and when it does not, the screen\nsays how the value is written.\n\nWhat is checked: a base is a whole number from 2 to 36; a number has only\ndigits of its base (at most 60, with a minus sign allowed); a number for\nRoman numerals is from 1 to 3999; a numeral uses only I, V, X, L, C, D and M,\nstays in range and is written the standard way. Anything else asks again;\nnothing cancels.\n\nSessions: `sessions/basic.in` converts 255 to base 16, FF to base 2, a\n30-digit number to base 36 and -42 to base 2, shows 2026 in the four common\nbases, writes 1994 and 3999 as Roman numerals and reads mcmxciv and XLII;\n`sessions/bad-input.in` types bases out of range or not numbers, digits that\ndo not belong to the base, a point, a doubled or lone minus sign, -0, numbers\noutside 1 to 3999, and numerals written the wrong way (IIII, IC, VV), too\nlarge (MMMM) or with other letters.\n\nBuilt on the verified corpus cases `base-converter` (digits by repeated\ndivision and remainder), `base-conversion-roundtrip` (writing a number in a\nbase and reading it back), `roman-numeral-converter` (the table walk) and\n`roman-to-integer` (the add-or-subtract reading rule).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P021 base converter: whole numbers between bases 2 to 36, a number in the\n# common bases at once, and Roman numerals both ways.\nimport bases\nimport roman\nimport text\n\n60 => longest\n\ndef ask_base(prompt):\n    # A base from 2 to 36, asked again until one is typed; -1 cancels.\n    while True:\n        text.trim(input(prompt)) => answer\n        if answer == \"\":\n            return 0 - 1\n        text.number(answer) => b\n        if b >= 2 and b <= 36:\n            return b\n        \"A base is a whole number from 2 to 36, or nothing to cancel.\" ^0\n\ndef parse_number(s, base):\n    # [negative, digits, \"\"] for a whole number written in base (a minus sign\n    # in front allowed), or [False, [], what is wrong].\n    text.upper(s) => s\n    False => negative\n    if len(s) > 0 and s[0] == \"-\":\n        True => negative\n        s[1:len(s)] => s\n    if s == \"\":\n        return [False, [], \"Type some digits.\"]\n    if len(s) > longest:\n        return [False, [], \"At most \" + str(longest) + \" digits.\"]\n    [] => digits\n    for c in s:\n        bases.digit_value(c) => v\n        if v == 0 - 1:\n            return [False, [], c + \" is not a digit; digits are 0-9, then A-Z for 10-35.\"]\n        if v >= base:\n            return [False, [], c + \" is not a digit in base \" + str(base) + \".\"]\n        digits + [v] => digits\n    bases.strip(digits) => digits\n    if digits == [0]:\n        False => negative\n    return [negative, digits, \"\"]\n\ndef ask_number(base):\n    # A number in base, asked again until one is typed; None cancels.\n    while True:\n        text.trim(input(\"number> \")) => answer\n        if answer == \"\":\n            return None\n        parse_number(answer, base) => r\n        if r[2] == \"\":\n            return r\n        r[2] ^0\n\ndef shown(n, digits):\n    # A number with its sign, from its sign and digit values.\n    if n[0]:\n        return \"-\" + bases.written(digits)\n    return bases.written(digits)\n\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Base converter ==\" ^0\n    \"1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\" or choice == \"2\":\n        ask_base(\"from base (2-36)> \") => base\n        None => n\n        if base != 0 - 1:\n            ask_number(base) => n\n        0 - 1 => target\n        if n != None and choice == \"1\":\n            ask_base(\"to base (2-36)> \") => target\n        if n == None or (choice == \"1\" and target == 0 - 1):\n            \"Cancelled.\" ^0\n        elif choice == \"1\":\n            (shown(n, n[1]) + \" (base \" + str(base) + \") = \" + shown(n, bases.convert(n[1], base, target)) + \" (base \" + str(target) + \")\") ^0\n        else:\n            (\"The number \" + shown(n, n[1]) + \" (base \" + str(base) + \") is:\") ^0\n            for b in [2, 8, 10, 16]:\n                (\"  \" + (\"%-9s\" % (\"base \" + str(b))) + shown(n, bases.convert(n[1], base, b))) ^0\n    elif choice == \"3\":\n        while True:\n            text.trim(input(\"number (1-3999)> \")) => answer\n            text.number(answer) => v\n            if answer == \"\" or (v >= 1 and v <= 3999):\n                break\n            \"Type a whole number from 1 to 3999, or nothing to cancel.\" ^0\n        if answer == \"\":\n            \"Cancelled.\" ^0\n        else:\n            (str(v) + \" = \" + roman.to_roman(v)) ^0\n    elif choice == \"4\":\n        while True:\n            text.upper(text.trim(input(\"numeral> \"))) => answer\n            if answer == \"\":\n                break\n            roman.from_roman(answer) => r\n            if r[1] == \"\":\n                break\n            r[1] ^0\n        if answer == \"\":\n            \"Cancelled.\" ^0\n        else:\n            (answer + \" = \" + str(r[0])) ^0\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import bases\nimport roman\nimport text\nlongest = 60\n\ndef ask_base(prompt):\n    while True:\n        answer = text.trim(input(prompt))\n        if answer == \"\":\n            return 0 - 1\n        b = text.number(answer)\n        if b >= 2 and b <= 36:\n            return b\n        print(\"A base is a whole number from 2 to 36, or nothing to cancel.\")\n\ndef parse_number(s, base):\n    s = text.upper(s)\n    negative = False\n    if len(s) > 0 and s[0] == \"-\":\n        negative = True\n        s = s[1:len(s)]\n    if s == \"\":\n        return [False, [], \"Type some digits.\"]\n    if len(s) > longest:\n        return [False, [], \"At most \" + str(longest) + \" digits.\"]\n    digits = []\n    for c in s:\n        v = bases.digit_value(c)\n        if v == 0 - 1:\n            return [False, [], c + \" is not a digit; digits are 0-9, then A-Z for 10-35.\"]\n        if v >= base:\n            return [False, [], c + \" is not a digit in base \" + str(base) + \".\"]\n        digits = digits + [v]\n    digits = bases.strip(digits)\n    if digits == [0]:\n        negative = False\n    return [negative, digits, \"\"]\n\ndef ask_number(base):\n    while True:\n        answer = text.trim(input(\"number> \"))\n        if answer == \"\":\n            return None\n        r = parse_number(answer, base)\n        if r[2] == \"\":\n            return r\n        print(r[2])\n\ndef shown(n, digits):\n    if n[0]:\n        return \"-\" + bases.written(digits)\n    return bases.written(digits)\n\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Base converter ==\")\n    print(\"1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\" or choice == \"2\":\n        base = ask_base(\"from base (2-36)> \")\n        n = None\n        if base != 0 - 1:\n            n = ask_number(base)\n        target = 0 - 1\n        if n != None and choice == \"1\":\n            target = ask_base(\"to base (2-36)> \")\n        if n == None or choice == \"1\" and target == 0 - 1:\n            print(\"Cancelled.\")\n        elif choice == \"1\":\n            print(shown(n, n[1]) + \" (base \" + str(base) + \") = \" + shown(n, bases.convert(n[1], base, target)) + \" (base \" + str(target) + \")\")\n        else:\n            print(\"The number \" + shown(n, n[1]) + \" (base \" + str(base) + \") is:\")\n            for b in [2, 8, 10, 16]:\n                print(\"  \" + \"%-9s\" % (\"base \" + str(b)) + shown(n, bases.convert(n[1], base, b)))\n    elif choice == \"3\":\n        while True:\n            answer = text.trim(input(\"number (1-3999)> \"))\n            v = text.number(answer)\n            if answer == \"\" or v >= 1 and v <= 3999:\n                break\n            print(\"Type a whole number from 1 to 3999, or nothing to cancel.\")\n        if answer == \"\":\n            print(\"Cancelled.\")\n        else:\n            print(str(v) + \" = \" + roman.to_roman(v))\n    elif choice == \"4\":\n        while True:\n            answer = text.upper(text.trim(input(\"numeral> \")))\n            if answer == \"\":\n                break\n            r = roman.from_roman(answer)\n            if r[1] == \"\":\n                break\n            print(r[1])\n        if answer == \"\":\n            print(\"Cancelled.\")\n        else:\n            print(answer + \" = \" + str(r[0]))\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "bases.eml",
      "eml": "# P021 base converter - whole numbers between bases 2 to 36. A number is kept\n# as its list of digit values, most significant first, and converted by long\n# division done by hand: divide the whole list by the new base, keep the\n# remainder as the next digit, and repeat until nothing is left. Every step\n# uses small numbers only, so numbers of any length come out exact.\n\n\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => digit_chars\n\ndef digit_value(c):\n    # The value of digit c (0-9, A-Z), or -1.\n    for i in [0:35]:\n        if digit_chars[i] == c:\n            return i\n    return 0 - 1\n\ndef divide(digits, base, by):\n    # [quotient digits, remainder] of the number digits (in base) divided by\n    # the small number by; the quotient has no leading zeros.\n    [] => q\n    0 => carry\n    for d in digits:\n        carry * base + d => current\n        int((current - current % by) / by) => part\n        current % by => carry\n        if len(q) > 0 or part > 0:\n            q + [part] => q\n    return [q, carry]\n\ndef convert(digits, base, new_base):\n    # The digits of the same number in new_base.\n    if len(digits) == 0:\n        return [0]\n    digits => rest\n    [] => out\n    while len(rest) > 0:\n        divide(rest, base, new_base) => r\n        [r[1]] + out => out\n        r[0] => rest\n    return out\n\ndef strip(digits):\n    # digits without leading zeros (but at least one digit).\n    0 => i\n    while i < len(digits) - 1 and digits[i] == 0:\n        i + 1 => i\n    return digits[i:len(digits)]\n\ndef written(digits):\n    \"\" => out\n    for d in digits:\n        out + digit_chars[d] => out\n    return out\n",
      "python": "digit_chars = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\ndef digit_value(c):\n    for i in range(0, 36):\n        if digit_chars[i] == c:\n            return i\n    return 0 - 1\n\ndef divide(digits, base, by):\n    q = []\n    carry = 0\n    for d in digits:\n        current = carry * base + d\n        part = int((current - current % by) / by)\n        carry = current % by\n        if len(q) > 0 or part > 0:\n            q = q + [part]\n    return [q, carry]\n\ndef convert(digits, base, new_base):\n    if len(digits) == 0:\n        return [0]\n    rest = digits\n    out = []\n    while len(rest) > 0:\n        r = divide(rest, base, new_base)\n        out = [r[1]] + out\n        rest = r[0]\n    return out\n\ndef strip(digits):\n    i = 0\n    while i < len(digits) - 1 and digits[i] == 0:\n        i = i + 1\n    return digits[i:len(digits)]\n\ndef written(digits):\n    out = \"\"\n    for d in digits:\n        out = out + digit_chars[d]\n    return out\n"
    },
    {
      "name": "roman.eml",
      "eml": "# P021 base converter - Roman numerals from 1 to 3999. Writing one walks a\n# table from the largest value down, the subtractive pairs (CM, CD, XC, XL,\n# IX, IV) being entries of the table. Reading one adds each symbol's value,\n# except that a symbol smaller than the one after it is subtracted. Reading\n# alone would accept IIII or IC, so a numeral counts only if writing its value\n# again gives exactly the same letters.\n\n[[1000, \"M\"], [900, \"CM\"], [500, \"D\"], [400, \"CD\"], [100, \"C\"], [90, \"XC\"],\n [50, \"L\"], [40, \"XL\"], [10, \"X\"], [9, \"IX\"], [5, \"V\"], [4, \"IV\"], [1, \"I\"]] => table\n\ndef to_roman(n):\n    # The Roman numeral for 1 <= n <= 3999.\n    \"\" => out\n    for row in table:\n        while n >= row[0]:\n            out + row[1] => out\n            n - row[0] => n\n    return out\n\ndef symbol_value(c):\n    for row in table:\n        if row[1] == c:\n            return row[0]\n    return 0\n\ndef read_value(s):\n    # The value of s by the add-or-subtract rule, or -1 if a letter is not a\n    # Roman symbol.\n    0 => total\n    for i in [0:len(s) - 1]:\n        symbol_value(s[i]) => v\n        if v == 0:\n            return 0 - 1\n        if i + 1 < len(s) and v < symbol_value(s[i + 1]):\n            total - v => total\n        else:\n            total + v => total\n    return total\n\ndef from_roman(s):\n    # [value, \"\"] for a numeral written the standard way, otherwise\n    # [0, what is wrong].\n    read_value(s) => v\n    if v == 0 - 1:\n        return [0, \"Roman numerals use only I, V, X, L, C, D and M.\"]\n    if v < 1 or v > 3999:\n        return [0, s + \" is not a numeral from I to MMMCMXCIX.\"]\n    if to_roman(v) != s:\n        return [0, s + \" is not written the standard way; \" + str(v) + \" is \" + to_roman(v) + \".\"]\n    return [v, \"\"]\n",
      "python": "table = [[1000, \"M\"], [900, \"CM\"], [500, \"D\"], [400, \"CD\"], [100, \"C\"], [90, \"XC\"], [50, \"L\"], [40, \"XL\"], [10, \"X\"], [9, \"IX\"], [5, \"V\"], [4, \"IV\"], [1, \"I\"]]\n\ndef to_roman(n):\n    out = \"\"\n    for row in table:\n        while n >= row[0]:\n            out = out + row[1]\n            n = n - row[0]\n    return out\n\ndef symbol_value(c):\n    for row in table:\n        if row[1] == c:\n            return row[0]\n    return 0\n\ndef read_value(s):\n    total = 0\n    for i in range(0, len(s)):\n        v = symbol_value(s[i])\n        if v == 0:\n            return 0 - 1\n        if i + 1 < len(s) and v < symbol_value(s[i + 1]):\n            total = total - v\n        else:\n            total = total + v\n    return total\n\ndef from_roman(s):\n    v = read_value(s)\n    if v == 0 - 1:\n        return [0, \"Roman numerals use only I, V, X, L, C, D and M.\"]\n    if v < 1 or v > 3999:\n        return [0, s + \" is not a numeral from I to MMMCMXCIX.\"]\n    if to_roman(v) != s:\n        return [0, s + \" is not written the standard way; \" + str(v) + \" is \" + to_roman(v) + \".\"]\n    return [v, \"\"]\n"
    },
    {
      "name": "text.eml",
      "eml": "# P021 base converter - reading what is typed. The interpreter that checks\n# every session does not run string methods yet, so the text handling is\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 upper(s):\n    # s with a-z turned into A-Z; everything else as it was.\n    \"abcdefghijklmnopqrstuvwxyz\" => lower_letters\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper_letters\n    \"\" => out\n    for c in s:\n        c => d\n        for i in [0:25]:\n            if lower_letters[i] == c:\n                upper_letters[i] => d\n        out + d => out\n    return out\n\ndef number(s):\n    # The value of s if it is digits only (at least one), otherwise -1.\n    if s == \"\":\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\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 upper(s):\n    lower_letters = \"abcdefghijklmnopqrstuvwxyz\"\n    upper_letters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    out = \"\"\n    for c in s:\n        d = c\n        for i in range(0, 26):\n            if lower_letters[i] == c:\n                d = upper_letters[i]\n        out = out + d\n    return out\n\ndef number(s):\n    if s == \"\":\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "9\n1\n1\n37\nx\n8\n129\n8.5\n--5\n-\n\n1\n2\n-0\n10\n3\n0\n4000\nabc\n2026\n4\nIIII\nIC\nVV\nMMMM\nABC\nIV\n4\n\n5\n",
      "screen": "\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 9\nPick a number from 1 to 5.\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 1\nA base is a whole number from 2 to 36, or nothing to cancel.\nfrom base (2-36)> 37\nA base is a whole number from 2 to 36, or nothing to cancel.\nfrom base (2-36)> x\nA base is a whole number from 2 to 36, or nothing to cancel.\nfrom base (2-36)> 8\nnumber> 129\n9 is not a digit in base 8.\nnumber> 8.5\n8 is not a digit in base 8.\nnumber> --5\n- is not a digit; digits are 0-9, then A-Z for 10-35.\nnumber> -\nType some digits.\nnumber> \nCancelled.\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 2\nnumber> -0\nto base (2-36)> 10\n0 (base 2) = 0 (base 10)\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 3\nnumber (1-3999)> 0\nType a whole number from 1 to 3999, or nothing to cancel.\nnumber (1-3999)> 4000\nType a whole number from 1 to 3999, or nothing to cancel.\nnumber (1-3999)> abc\nType a whole number from 1 to 3999, or nothing to cancel.\nnumber (1-3999)> 2026\n2026 = MMXXVI\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 4\nnumeral> IIII\nIIII is not written the standard way; 4 is IV.\nnumeral> IC\nIC is not written the standard way; 99 is XCIX.\nnumeral> VV\nVV is not written the standard way; 10 is X.\nnumeral> MMMM\nMMMM is not a numeral from I to MMMCMXCIX.\nnumeral> ABC\nRoman numerals use only I, V, X, L, C, D and M.\nnumeral> IV\nIV = 4\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 4\nnumeral> \nCancelled.\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n10\n255\n16\n1\n16\nff\n2\n1\n10\n123456789012345678901234567890\n36\n1\n10\n-42\n2\n2\n10\n2026\n3\n1994\n3\n3999\n4\nmcmxciv\n4\nXLII\n5\n",
      "screen": "\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 10\nnumber> 255\nto base (2-36)> 16\n255 (base 10) = FF (base 16)\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 16\nnumber> ff\nto base (2-36)> 2\nFF (base 16) = 11111111 (base 2)\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 10\nnumber> 123456789012345678901234567890\nto base (2-36)> 36\n123456789012345678901234567890 (base 10) = BYW97UM9S91DLZ68TSI (base 36)\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 1\nfrom base (2-36)> 10\nnumber> -42\nto base (2-36)> 2\n-42 (base 10) = -101010 (base 2)\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 2\nfrom base (2-36)> 10\nnumber> 2026\nThe number 2026 (base 10) is:\n  base 2   11111101010\n  base 8   3752\n  base 10  2026\n  base 16  7EA\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 3\nnumber (1-3999)> 1994\n1994 = MCMXCIV\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 3\nnumber (1-3999)> 3999\n3999 = MMMCMXCIX\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 4\nnumeral> mcmxciv\nMCMXCIV = 1994\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 4\nnumeral> XLII\nXLII = 42\n\n== Base converter ==\n1) convert a number  2) in bases 2, 8, 10, 16  3) to Roman  4) from Roman  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "base-converter",
      "caseId": "014-base-converter",
      "title": "Base converter (decimal to binary/octal)"
    },
    {
      "slug": "base-conversion-roundtrip",
      "caseId": "232-base-conversion-roundtrip",
      "title": "Round-tripping and canonicality are different claims"
    },
    {
      "slug": "roman-numeral-converter",
      "caseId": "092-roman-numeral-converter",
      "title": "Roman numeral converter"
    },
    {
      "slug": "roman-to-integer",
      "caseId": "103-roman-to-integer",
      "title": "Roman numeral to integer"
    }
  ],
  "updated": "2026-10-03"
}
