File: roman_numerals.py

package info (click to toggle)
pyparsing 3.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,928 kB
  • sloc: python: 25,633; ansic: 422; makefile: 22
file content (139 lines) | stat: -rw-r--r-- 3,657 bytes parent folder | download
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()