1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
# romanNumerals.py
#
# Copyright (c) 2006, 2019, Paul McGuire
#
import pyparsing as pp
def roman_numeral_literal(numeral_string, value):
return (
pp.Literal(numeral_string)
.set_parse_action(pp.replace_with(value))
.leave_whitespace()
)
one = roman_numeral_literal("I", 1)
four = roman_numeral_literal("IV", 4)
five = roman_numeral_literal("V", 5)
nine = roman_numeral_literal("IX", 9)
ten = roman_numeral_literal("X", 10)
forty = roman_numeral_literal("XL", 40)
fifty = roman_numeral_literal("L", 50)
ninety = roman_numeral_literal("XC", 90)
onehundred = roman_numeral_literal("C", 100)
fourhundred = roman_numeral_literal("CD", 400)
fivehundred = roman_numeral_literal("D", 500)
ninehundred = roman_numeral_literal("CM", 900)
onethousand = roman_numeral_literal("M", 1000)
# lenient parser - passes all legal Roman numerals but does not detect illegal
# numeral = (
# onethousand
# | ninehundred
# | fivehundred
# | fourhundred
# | onehundred
# | ninety
# | fifty
# | forty
# | ten
# | nine
# | five
# | four
# | one
# )
#
# roman_numeral = numeral[1, ...].set_parse_action(sum)
# strict parser - rejects illegal Roman numerals
roman_numeral = (
onethousand[...]
+ (ninehundred | fourhundred | fivehundred[0, 1] + onehundred[0, 3])[0, 1]
+ (ninety | forty | fifty[0, 1] + ten[0, 3])[0, 1]
+ (nine | four | five[0, 1] + one[0, 3])[0, 1]
).set_parse_action(sum)
pp.autoname_elements()
# uncomment to generate railroad diagram
# roman_numeral.create_diagram("romanNumerals.html")
# unit tests
def make_roman_numeral(n):
def add_digits(n, limit, c, s):
while n >= limit:
n -= limit
s += c
return n, s
ret = ""
n, ret = add_digits(n, 1000, "M", ret)
n, ret = add_digits(n, 900, "CM", ret)
n, ret = add_digits(n, 500, "D", ret)
n, ret = add_digits(n, 400, "CD", ret)
n, ret = add_digits(n, 100, "C", ret)
n, ret = add_digits(n, 90, "XC", ret)
n, ret = add_digits(n, 50, "L", ret)
n, ret = add_digits(n, 40, "XL", ret)
n, ret = add_digits(n, 10, "X", ret)
n, ret = add_digits(n, 9, "IX", ret)
n, ret = add_digits(n, 5, "V", ret)
n, ret = add_digits(n, 4, "IV", ret)
n, ret = add_digits(n, 1, "I", ret)
return ret
def main():
# make a string of all roman numerals from I to MMMMM
tests = " ".join(make_roman_numeral(i) for i in range(1, 5000 + 1))
# parse each roman numeral, and populate map for validation below
roman_int_map = {}
for expected, (t, s, e) in enumerate(roman_numeral.scan_string(tests), start=1):
orig = tests[s:e]
assert t[0] == expected, f"==> Incorrect result for {orig}: {t}"
roman_int_map[orig] = t[0]
def verify_value(s, tokens):
expected_value = roman_int_map[s]
if tokens[0] != expected_value:
raise Exception(
f"incorrect value for {s} ({tokens[0]}), expected {expected_value}"
)
success1, _ = roman_numeral.runTests(
"""\
XVI
XXXIX
XIV
XIX
MCMLXXX
MMVI
MMMMM
""",
parse_all=True,
post_parse=verify_value,
)
assert success1, "failed to parse one or more legal Roman numerals"
print("\nRun failure tests")
success2, _ = roman_numeral.runTests(
"""\
# too many X's
XXXX
# X after XL
XLX
""",
parse_all=True,
failure_tests=True,
)
assert success2, "parsed one or more illegal Roman numerals"
if __name__ == "__main__":
main()
|