{
  "id": "P061",
  "slug": "roman-numerals",
  "key": "P061-roman-numerals",
  "title": "Roman numerals",
  "summary": "Numbers from 1 to 3999 to Roman numerals and back, each split into its parts; a check that a numeral is written the standard way - IIII and IC read, but are not - with the right way given; and adding and subtracting Roman numerals.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P061 - Roman numerals\n\nNumbers from 1 to 3999 to Roman numerals and back, each numeral split into\nits parts. A numeral typed in is checked for the standard way of writing\nit; when it reads but is not standard, the right way is given. Roman\nnumerals can also be added and subtracted.\n\n- `main.eml` - the menu, reading numbers and numerals, and the messages\n- `roman.eml` - both directions, the standard-form check, and splitting a\n  numeral into parts\n\nHow each part works:\n\n- To a numeral: walk a table of values from 1000 (M) down to 1 (I) and\n  take the largest that still fits, again and again. The subtractive pairs\n  - CM, CD, XC, XL, IX, IV - are entries of the table, as in the corpus\n  case `roman-numeral-converter`, so no special case is needed.\n- To a number: add each letter's value, except subtract a letter smaller\n  than the one after it, the reading rule of the corpus case\n  `roman-to-integer`.\n- That rule also reads numerals that are not written the standard way:\n  IIII as 4, IC as 99, VX as 5. So a numeral counts as standard only when\n  turning its value back into a numeral gives the same letters - the round\n  trip the two corpus cases make between them. Lower case is read as\n  upper case.\n- Roman numerals have no zero and no negatives, and past 3999 they need a\n  bar over the letters, which plain text cannot show. So sums and\n  differences outside 1 to 3999 are given as numbers only.\n\nWhat is checked: menu choices 1 to 5; numbers from 1 to 3999; numerals\nusing only I V X L C D M, in the standard form, from I to MMMCMXCIX. An\nempty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- 1994, 2026 and 3999 to numerals, with their parts;\n- mmxxvi, in lower case, and XLII back to numbers;\n- XIV + XXVIII = XLII, and MMXXVI - MCMXCIV = XXXII.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- the numbers 0, 4000, -5 and abc;\n- the numerals IIII, IC, VX and IIX, which read but are not standard,\n  MMMM (4000), and ABC;\n- MMM + M, which passes 3999;\n- V - V and X - L, which have no numeral.\n\nBuilt on the verified corpus cases `roman-numeral-converter` (a greedy\nwalk down a table that includes the subtractive pairs) and\n`roman-to-integer` (the reverse reading rule, checked as a round trip).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P061 Roman numerals: numbers to Roman numerals and back, a check that a\n# numeral is written the standard way (and the right way when it is not),\n# and adding and subtracting Roman numerals.\nimport roman\n\ndef trim(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 number(s):\n    if s == \"\" or len(s) > 6:\n        return -1\n    0 => n\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n * 10 + int(c) => n\n    return n\n\ndef breakdown(s):\n    \"\" => out\n    for p in roman.parts(s):\n        if out != \"\":\n            out + \" + \" => out\n        out + p[1] + \" (\" + str(p[0]) + \")\" => out\n    return out\n\ndef ask_numeral(prompt):\n    # A standard Roman numeral, as [text, value], or [] when the answer is\n    # empty. A numeral that reads but is not standard is explained and\n    # asked for again.\n    while True:\n        roman.upper(trim(input(prompt))) => s\n        if s == \"\":\n            return []\n        roman.value_of(s) => v\n        if v == -1:\n            \"Use only the letters I V X L C D M.\" ^0\n        elif roman.standard(s):\n            return [s, v]\n        elif v < 1 or v > 3999:\n            (s + \" reads as \" + str(v) + \", but Roman numerals here go from I (1) to MMMCMXCIX (3999).\") ^0\n        else:\n            (s + \" reads as \" + str(v) + \", but is not the standard way to write it: \" + str(v) + \" is \" + roman.to_roman(v) + \".\") ^0\n\ndef to_roman():\n    while True:\n        trim(input(\"number (1 to 3999)> \")) => answer\n        if answer == \"\":\n            \"Cancelled.\" ^0\n            return\n        number(answer) => n\n        if n >= 1 and n <= 3999:\n            break\n        \"Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\" ^0\n    roman.to_roman(n) => r\n    (str(n) + \" is \" + r + \" = \" + breakdown(r) + \".\") ^0\n\ndef from_roman():\n    ask_numeral(\"Roman numeral> \") => r\n    if len(r) == 0:\n        \"Cancelled.\" ^0\n        return\n    (r[0] + \" is \" + str(r[1]) + \" = \" + breakdown(r[0]) + \".\") ^0\n\ndef arithmetic(subtract):\n    ask_numeral(\"first numeral> \") => a\n    if len(a) == 0:\n        \"Cancelled.\" ^0\n        return\n    ask_numeral(\"second numeral> \") => b\n    if len(b) == 0:\n        \"Cancelled.\" ^0\n        return\n    a[1] + b[1] => v\n    \"+\" => op\n    if subtract:\n        a[1] - b[1] => v\n        \"-\" => op\n    (a[0] + \" \" + op + \" \" + b[0] + \" is \" + str(a[1]) + \" \" + op + \" \" + str(b[1]) + \" = \" + str(v) + \".\") ^0\n    if v < 1:\n        \"There is no Roman numeral for that: they start at I, with no zero or negatives.\" ^0\n    elif v > 3999:\n        \"That is past MMMCMXCIX (3999), the largest numeral without a bar over the letters.\" ^0\n    else:\n        (\"In Roman numerals: \" + roman.to_roman(v) + \".\") ^0\n\n\"== Roman numerals ==\" ^0\n\"Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) to Roman  2) from Roman  3) add  4) subtract  5) quit\" ^0\n    trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        to_roman()\n    elif choice == \"2\":\n        from_roman()\n    elif choice == \"3\":\n        arithmetic(False)\n    elif choice == \"4\":\n        arithmetic(True)\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import roman\n\ndef 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 number(s):\n    if s == \"\" or len(s) > 6:\n        return -1\n    n = 0\n    for c in s:\n        if not (c >= \"0\" and c <= \"9\"):\n            return -1\n        n = n * 10 + int(c)\n    return n\n\ndef breakdown(s):\n    out = \"\"\n    for p in roman.parts(s):\n        if out != \"\":\n            out = out + \" + \"\n        out = out + p[1] + \" (\" + str(p[0]) + \")\"\n    return out\n\ndef ask_numeral(prompt):\n    while True:\n        s = roman.upper(trim(input(prompt)))\n        if s == \"\":\n            return []\n        v = roman.value_of(s)\n        if v == -1:\n            print(\"Use only the letters I V X L C D M.\")\n        elif roman.standard(s):\n            return [s, v]\n        elif v < 1 or v > 3999:\n            print(s + \" reads as \" + str(v) + \", but Roman numerals here go from I (1) to MMMCMXCIX (3999).\")\n        else:\n            print(s + \" reads as \" + str(v) + \", but is not the standard way to write it: \" + str(v) + \" is \" + roman.to_roman(v) + \".\")\n\ndef to_roman():\n    while True:\n        answer = trim(input(\"number (1 to 3999)> \"))\n        if answer == \"\":\n            print(\"Cancelled.\")\n            return\n        n = number(answer)\n        if n >= 1 and n <= 3999:\n            break\n        print(\"Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\")\n    r = roman.to_roman(n)\n    print(str(n) + \" is \" + r + \" = \" + breakdown(r) + \".\")\n\ndef from_roman():\n    r = ask_numeral(\"Roman numeral> \")\n    if len(r) == 0:\n        print(\"Cancelled.\")\n        return\n    print(r[0] + \" is \" + str(r[1]) + \" = \" + breakdown(r[0]) + \".\")\n\ndef arithmetic(subtract):\n    a = ask_numeral(\"first numeral> \")\n    if len(a) == 0:\n        print(\"Cancelled.\")\n        return\n    b = ask_numeral(\"second numeral> \")\n    if len(b) == 0:\n        print(\"Cancelled.\")\n        return\n    v = a[1] + b[1]\n    op = \"+\"\n    if subtract:\n        v = a[1] - b[1]\n        op = \"-\"\n    print(a[0] + \" \" + op + \" \" + b[0] + \" is \" + str(a[1]) + \" \" + op + \" \" + str(b[1]) + \" = \" + str(v) + \".\")\n    if v < 1:\n        print(\"There is no Roman numeral for that: they start at I, with no zero or negatives.\")\n    elif v > 3999:\n        print(\"That is past MMMCMXCIX (3999), the largest numeral without a bar over the letters.\")\n    else:\n        print(\"In Roman numerals: \" + roman.to_roman(v) + \".\")\n\nprint(\"== Roman numerals ==\")\nprint(\"Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) to Roman  2) from Roman  3) add  4) subtract  5) quit\")\n    choice = trim(input(\"choice> \"))\n    if choice == \"1\":\n        to_roman()\n    elif choice == \"2\":\n        from_roman()\n    elif choice == \"3\":\n        arithmetic(False)\n    elif choice == \"4\":\n        arithmetic(True)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "roman.eml",
      "eml": "# P061 Roman numerals - the two directions and the check between them.\n\ndef table():\n    # The values and symbols from largest to smallest, the subtractive pairs\n    # (CM, CD, XC, XL, IX, IV) as entries of their own, as in the corpus case\n    # roman-numeral-converter.\n    return [[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\"]]\n\ndef to_roman(n):\n    # 1 to 3999 as a Roman numeral: take the largest value that still fits,\n    # again and again.\n    \"\" => out\n    n => left\n    for row in table():\n        while left >= row[0]:\n            out + row[1] => out\n            left - row[0] => left\n    return out\n\ndef letter_value(ch):\n    # The value of one letter, upper or lower case, or 0 for anything else.\n    \"IVXLCDM\" => big\n    \"ivxlcdm\" => small\n    [1, 5, 10, 50, 100, 500, 1000] => values\n    for k in [0:6]:\n        if ch == big[k] or ch == small[k]:\n            return values[k]\n    return 0\n\ndef value_of(s):\n    # The value by the reading rule of the corpus case roman-to-integer: add\n    # each letter, except subtract one that is smaller than the letter after\n    # it. Returns -1 when a character is not a Roman letter. The rule reads\n    # non-standard numerals too - IIII as 4, IC as 99 - so a value alone does\n    # not say the numeral was written right.\n    0 => total\n    for i in [0:len(s) - 1]:\n        letter_value(s[i]) => v\n        if v == 0:\n            return -1\n        0 => next_v\n        if i + 1 < len(s):\n            letter_value(s[i + 1]) => next_v\n        if v < next_v:\n            total - v => total\n        else:\n            total + v => total\n    return total\n\ndef upper(s):\n    \"\" => out\n    for ch in s:\n        ch => c\n        for k in [0:6]:\n            if ch == \"ivxlcdm\"[k]:\n                \"IVXLCDM\"[k] => c\n        out + c => out\n    return out\n\ndef standard(s):\n    # Written the standard way when converting its value back gives the same\n    # letters - the round trip the two corpus cases make between them.\n    value_of(s) => v\n    if v < 1 or v > 3999:\n        return False\n    return to_roman(v) == upper(s)\n\ndef parts(s):\n    # A standard numeral split into its table entries, largest first:\n    # MCMXCIV is M, CM, XC, IV. Two-letter entries are tried before one.\n    [] => out\n    upper(s) => t\n    0 => i\n    while i < len(t):\n        False => found\n        for row in table():\n            if not found and len(row[1]) == 2 and i + 1 < len(t) and t[i:i + 2] == row[1]:\n                out + [row] => out\n                i + 2 => i\n                True => found\n        if not found:\n            for row in table():\n                if not found and len(row[1]) == 1 and t[i] == row[1]:\n                    out + [row] => out\n                    i + 1 => i\n                    True => found\n    return out\n",
      "python": "def table():\n    return [[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    left = n\n    for row in table():\n        while left >= row[0]:\n            out = out + row[1]\n            left = left - row[0]\n    return out\n\ndef letter_value(ch):\n    big = \"IVXLCDM\"\n    small = \"ivxlcdm\"\n    values = [1, 5, 10, 50, 100, 500, 1000]\n    for k in range(0, 7):\n        if ch == big[k] or ch == small[k]:\n            return values[k]\n    return 0\n\ndef value_of(s):\n    total = 0\n    for i in range(0, len(s)):\n        v = letter_value(s[i])\n        if v == 0:\n            return -1\n        next_v = 0\n        if i + 1 < len(s):\n            next_v = letter_value(s[i + 1])\n        if v < next_v:\n            total = total - v\n        else:\n            total = total + v\n    return total\n\ndef upper(s):\n    out = \"\"\n    for ch in s:\n        c = ch\n        for k in range(0, 7):\n            if ch == \"ivxlcdm\"[k]:\n                c = \"IVXLCDM\"[k]\n        out = out + c\n    return out\n\ndef standard(s):\n    v = value_of(s)\n    if v < 1 or v > 3999:\n        return False\n    return to_roman(v) == upper(s)\n\ndef parts(s):\n    out = []\n    t = upper(s)\n    i = 0\n    while i < len(t):\n        found = False\n        for row in table():\n            if not found and len(row[1]) == 2 and i + 1 < len(t) and t[i:i + 2] == row[1]:\n                out = out + [row]\n                i = i + 2\n                found = True\n        if not found:\n            for row in table():\n                if not found and len(row[1]) == 1 and t[i] == row[1]:\n                    out = out + [row]\n                    i = i + 1\n                    found = True\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\n0\n4000\n-5\nabc\n\n2\nIIII\nIC\nVX\nMMMM\nABC\nIIX\n\n3\nMMM\nM\n4\nV\nV\n4\nX\nL\n5\n",
      "screen": "== Roman numerals ==\nNumbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 1\nnumber (1 to 3999)> 0\nType a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\nnumber (1 to 3999)> 4000\nType a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\nnumber (1 to 3999)> -5\nType a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\nnumber (1 to 3999)> abc\nType a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.\nnumber (1 to 3999)> \nCancelled.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 2\nRoman numeral> IIII\nIIII reads as 4, but is not the standard way to write it: 4 is IV.\nRoman numeral> IC\nIC reads as 99, but is not the standard way to write it: 99 is XCIX.\nRoman numeral> VX\nVX reads as 5, but is not the standard way to write it: 5 is V.\nRoman numeral> MMMM\nMMMM reads as 4000, but Roman numerals here go from I (1) to MMMCMXCIX (3999).\nRoman numeral> ABC\nUse only the letters I V X L C D M.\nRoman numeral> IIX\nIIX reads as 10, but is not the standard way to write it: 10 is X.\nRoman numeral> \nCancelled.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 3\nfirst numeral> MMM\nsecond numeral> M\nMMM + M is 3000 + 1000 = 4000.\nThat is past MMMCMXCIX (3999), the largest numeral without a bar over the letters.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 4\nfirst numeral> V\nsecond numeral> V\nV - V is 5 - 5 = 0.\nThere is no Roman numeral for that: they start at I, with no zero or negatives.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 4\nfirst numeral> X\nsecond numeral> L\nX - L is 10 - 50 = -40.\nThere is no Roman numeral for that: they start at I, with no zero or negatives.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n1994\n1\n2026\n1\n3999\n2\nmmxxvi\n2\nXLII\n3\nXIV\nXXVIII\n4\nMMXXVI\nMCMXCIV\n5\n",
      "screen": "== Roman numerals ==\nNumbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 1\nnumber (1 to 3999)> 1994\n1994 is MCMXCIV = M (1000) + CM (900) + XC (90) + IV (4).\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 1\nnumber (1 to 3999)> 2026\n2026 is MMXXVI = M (1000) + M (1000) + X (10) + X (10) + V (5) + I (1).\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 1\nnumber (1 to 3999)> 3999\n3999 is MMMCMXCIX = M (1000) + M (1000) + M (1000) + CM (900) + XC (90) + IX (9).\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 2\nRoman numeral> mmxxvi\nMMXXVI is 2026 = M (1000) + M (1000) + X (10) + X (10) + V (5) + I (1).\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 2\nRoman numeral> XLII\nXLII is 42 = XL (40) + I (1) + I (1).\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 3\nfirst numeral> XIV\nsecond numeral> XXVIII\nXIV + XXVIII is 14 + 28 = 42.\nIn Roman numerals: XLII.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 4\nfirst numeral> MMXXVI\nsecond numeral> MCMXCIV\nMMXXVI - MCMXCIV is 2026 - 1994 = 32.\nIn Roman numerals: XXXII.\n\n1) to Roman  2) from Roman  3) add  4) subtract  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "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-14"
}
