diff --git a/python/say/.exercism/metadata.json b/python/say/.exercism/metadata.json new file mode 100644 index 0000000..18b6fc4 --- /dev/null +++ b/python/say/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"say","id":"c5c81bf1586047a8bf7913ce10e4e7c5","url":"https://exercism.io/my/solutions/c5c81bf1586047a8bf7913ce10e4e7c5","handle":"Xevion","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/say/README.md b/python/say/README.md new file mode 100644 index 0000000..9d353a7 --- /dev/null +++ b/python/say/README.md @@ -0,0 +1,112 @@ +# Say + +Given a number from 0 to 999,999,999,999, spell out that number in English. + +## Step 1 + +Handle the basic case of 0 through 99. + +If the input to the program is `22`, then the output should be +`'twenty-two'`. + +Your program should complain loudly if given a number outside the +blessed range. + +Some good test cases for this program are: + +- 0 +- 14 +- 50 +- 98 +- -1 +- 100 + +### Extension + +If you're on a Mac, shell out to Mac OS X's `say` program to talk out +loud. If you're on Linux or Windows, eSpeakNG may be available with the command `espeak`. + +## Step 2 + +Implement breaking a number up into chunks of thousands. + +So `1234567890` should yield a list like 1, 234, 567, and 890, while the +far simpler `1000` should yield just 1 and 0. + +The program must also report any values that are out of range. + +## Step 3 + +Now handle inserting the appropriate scale word between those chunks. + +So `1234567890` should yield `'1 billion 234 million 567 thousand 890'` + +The program must also report any values that are out of range. It's +fine to stop at "trillion". + +## Step 4 + +Put it all together to get nothing but plain English. + +`12345` should give `twelve thousand three hundred forty-five`. + +The program must also report any values that are out of range. + +### Extensions + +Use _and_ (correctly) when spelling out the number in English: + +- 14 becomes "fourteen". +- 100 becomes "one hundred". +- 120 becomes "one hundred and twenty". +- 1002 becomes "one thousand and two". +- 1323 becomes "one thousand three hundred and twenty-three". + +## 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 say_test.py` +- Python 3.4+: `pytest say_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 say_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/say` 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 + +A variation on JavaRanch CattleDrive, exercise 4a [http://www.javaranch.com/say.jsp](http://www.javaranch.com/say.jsp) + +## 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/say/say.py b/python/say/say.py new file mode 100644 index 0000000..35c4cb7 --- /dev/null +++ b/python/say/say.py @@ -0,0 +1,60 @@ +sub = { + 1_000_000_000_000 : 'trillion', + 1_000_000_000 : 'billion', + 1_000_000 : 'million', + 1000 : 'thousand', + 100 : 'hundred', + 90 : 'ninety', + 80 : 'eighty', + 70 : 'seventy', + 60 : 'sixty', + 50 : 'fifty', + 40 : 'forty', + 30 : 'thirty', + 20 : 'twenty', +} + +number = { + 12 : 'twelve', + 11 : 'eleven', + 10 : 'ten', + 9 : 'nine', + 8 : 'eight', + 7 : 'seven', + 6 : 'six', + 5 : 'five', + 4 : 'four', + 3 : 'three', + 2 : 'two', + 1 : 'one', + 0 : 'zero' +} + +teen = {i + 10 : number[i] + 'teen' for i in range(3, 10)} +teen[13] = 'thirteen';teen[15] = 'fifteen';teen[18] = 'eighteen' + +def say(n): + if n < 0: + raise ValueError(f'Cannot parse negative number \'{n}\'') + # return 'negative ' + say(abs(n)) + elif n < 13: + return number[n] + elif n < 20: + return teen[n] + elif n < 100: + return sub[n // 10 * 10] + (('-' + number[n % 10]) if n % 10 != 0 else '') + elif max(*tuple(sub.keys()), n) == n: + raise ValueError(f'Cannot support large number \'{n}\'') + else: + res = [] + while n > 0: + if n >= 100: + temp = max(sub.keys(), key=lambda item: 0 if item > n else item) + prefix = n // temp + n = n % temp + res.append(say(prefix) + ' ' + sub[temp]) + else: + res.append(say(n)) + n = 0 + if len(res) < 2: return ' '.join(res) + return ' '.join(res[:-1]) + ' and ' + res[-1] \ No newline at end of file diff --git a/python/say/say_test.py b/python/say/say_test.py new file mode 100644 index 0000000..e598e46 --- /dev/null +++ b/python/say/say_test.py @@ -0,0 +1,75 @@ +import unittest + +from say import say + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v1.2.0 + +class SayTest(unittest.TestCase): + def test_zero(self): + self.assertEqual(say(0), "zero") + + def test_one(self): + self.assertEqual(say(1), "one") + + def test_fourteen(self): + self.assertEqual(say(14), "fourteen") + + def test_twenty(self): + self.assertEqual(say(20), "twenty") + + def test_twenty_two(self): + self.assertEqual(say(22), "twenty-two") + + def test_one_hundred(self): + self.assertEqual(say(100), "one hundred") + + # additional track specific test + def test_one_hundred_twenty_three(self): + self.assertEqual(say(123), "one hundred and twenty-three") + + def test_one_thousand(self): + self.assertEqual(say(1000), "one thousand") + + def test_one_thousand_two_hundred_thirty_four(self): + self.assertEqual(say(1234), "one thousand two hundred and thirty-four") + + def test_one_million(self): + self.assertEqual(say(1000000), "one million") + + def test_1002345(self): + self.assertEqual( + say(1002345), + "one million two thousand three hundred and forty-five") + + def test_one_billion(self): + self.assertEqual(say(1000000000), "one billion") + + def test_987654321123(self): + self.assertEqual( + say(987654321123), ("nine hundred and eighty-seven billion " + "six hundred and fifty-four million " + "three hundred and twenty-one thousand " + "one hundred and twenty-three")) + + def test_number_too_large(self): + with self.assertRaisesWithMessage(ValueError): + say(1000000000000) + + def test_number_negative(self): + with self.assertRaisesWithMessage(ValueError): + say(-1) + + # Utility functions + def setUp(self): + try: + self.assertRaisesRegex + except AttributeError: + self.assertRaisesRegex = self.assertRaisesRegexp + + def assertRaisesWithMessage(self, exception): + return self.assertRaisesRegex(exception, r".+") + + +if __name__ == '__main__': + unittest.main()