diff --git a/python/roman-numerals/.exercism/metadata.json b/python/roman-numerals/.exercism/metadata.json new file mode 100644 index 0000000..ee7846f --- /dev/null +++ b/python/roman-numerals/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"roman-numerals","id":"878a032e3e874bc3bf6fbe5d6a087962","url":"https://exercism.io/my/solutions/878a032e3e874bc3bf6fbe5d6a087962","handle":"Xevion","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/roman-numerals/README.md b/python/roman-numerals/README.md new file mode 100644 index 0000000..33d070a --- /dev/null +++ b/python/roman-numerals/README.md @@ -0,0 +1,92 @@ +# Roman Numerals + +Write a function to convert from normal numbers to Roman Numerals. + +The Romans were a clever bunch. They conquered most of Europe and ruled +it for hundreds of years. They invented concrete and straight roads and +even bikinis. One thing they never discovered though was the number +zero. This made writing and dating extensive histories of their exploits +slightly more challenging, but the system of numbers they came up with +is still in use today. For example the BBC uses Roman numerals to date +their programmes. + +The Romans wrote numbers using letters - I, V, X, L, C, D, M. (notice +these letters have lots of straight lines and are hence easy to hack +into stone tablets). + +```text + 1 => I +10 => X + 7 => VII +``` + +There is no need to be able to convert numbers larger than about 3000. +(The Romans themselves didn't tend to go any higher) + +Wikipedia says: Modern Roman numerals ... are written by expressing each +digit separately starting with the left most digit and skipping any +digit with a value of zero. + +To see this in practice, consider the example of 1990. + +In Roman numerals 1990 is MCMXC: + +1000=M +900=CM +90=XC + +2008 is written as MMVIII: + +2000=MM +8=VIII + +See also: http://www.novaroma.org/via_romana/numbers.html + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)): + +- Python 2.7: `py.test roman_numerals_test.py` +- Python 3.4+: `pytest roman_numerals_test.py` + +Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version): +`python -m pytest roman_numerals_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/roman-numerals` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Source + +The Roman Numeral Kata [http://codingdojo.org/cgi-bin/index.pl?KataRomanNumerals](http://codingdojo.org/cgi-bin/index.pl?KataRomanNumerals) + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/python/roman-numerals/roman_numerals.py b/python/roman-numerals/roman_numerals.py new file mode 100644 index 0000000..a016c88 --- /dev/null +++ b/python/roman-numerals/roman_numerals.py @@ -0,0 +1,27 @@ +numerals = {1 : 'I', 5 : 'V', 10 : 'X', 50 : 'L', 100 : 'C', 500 : 'D', 1000 : 'M', 5_000 : '_V', 10_000 : '_X', 50_000 : '_L', 100_000 : '_C', 500_000 : '_D', 1_000_000 : '_M'} +match = {'V' : 'I', 'X' : 'I', 'L' : 'X', 'C' : 'X', 'D' : 'C', 'M' : 'C'} + +def first(n, s=0): + return (n, 10 ** s) if n < 10 else first(n // 10, s=s+1) + +def roman(n): + if n % 10 != 0 and len(str(n).strip('0')) > 1: + temp = map(lambda item : int(item[1] + '0' * (len(str(n)) - 1 - item[0])), enumerate(str(n))) + return ''.join(map(roman, temp)) + if n in numerals: return numerals[n] + if n > 3 and (first(n)[0] + 1) * first(n)[1] in numerals: + top = (first(n)[0] + 1) * first(n)[1] + if n >= 4000: + if numerals[top].startswith('_'): + return '_' + match[numerals[top][1:]] + numerals[top] + return match[numerals[top]] + numerals[top] + else: + res = '' + while n > 0: + best = max(numerals.keys(), key=lambda item : 0 if item > n else item) + n -= best + res += numerals[best] + return res + +for i in range(2950, 35000, 13): + print('{} -> {}'.format(i, roman(i))) \ No newline at end of file diff --git a/python/roman-numerals/roman_numerals_test.py b/python/roman-numerals/roman_numerals_test.py new file mode 100644 index 0000000..4dc50dd --- /dev/null +++ b/python/roman-numerals/roman_numerals_test.py @@ -0,0 +1,37 @@ +import unittest + +import roman_numerals + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0 + + +class RomanNumeralsTest(unittest.TestCase): + numerals = { + 1: 'I', + 2: 'II', + 3: 'III', + 4: 'IV', + 5: 'V', + 6: 'VI', + 9: 'IX', + 27: 'XXVII', + 48: 'XLVIII', + 49: 'XLIX', + 59: 'LIX', + 93: 'XCIII', + 141: 'CXLI', + 163: 'CLXIII', + 402: 'CDII', + 575: 'DLXXV', + 911: 'CMXI', + 1024: 'MXXIV', + 3000: 'MMM', + } + + def test_numerals(self): + for arabic, numeral in self.numerals.items(): + self.assertEqual(roman_numerals.roman(arabic), numeral) + + +if __name__ == '__main__': + unittest.main()