From 58315f6fe61dbce2dff6e47febae81c4d245b0a1 Mon Sep 17 00:00:00 2001 From: Xevion Date: Wed, 24 Jul 2019 17:24:22 -0500 Subject: [PATCH] ledger exercise entry class --- .vscode/settings.json | 8 +- python/COMMENTS.md | 32 ++- python/food-chain/food_chain.py | 7 +- python/ledger/.exercism/metadata.json | 1 + python/ledger/README.md | 60 ++++++ python/ledger/ledger.py | 296 ++++++++++++++++++++++++++ python/ledger/ledger_test.py | 147 +++++++++++++ 7 files changed, 547 insertions(+), 4 deletions(-) create mode 100644 python/ledger/.exercism/metadata.json create mode 100644 python/ledger/README.md create mode 100644 python/ledger/ledger.py create mode 100644 python/ledger/ledger_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json index e27478f..01d7645 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,11 @@ "python.pythonPath": "A:\\Installations\\Anaconda\\python.exe", "python.linting.flake8Enabled": true, "python.linting.enabled": false, - "python.linting.pylintEnabled": false + "python.linting.pylintEnabled": false, + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true } \ No newline at end of file diff --git a/python/COMMENTS.md b/python/COMMENTS.md index e39c055..d244592 100644 --- a/python/COMMENTS.md +++ b/python/COMMENTS.md @@ -2,7 +2,7 @@ This page represents all my comments from my solutions currently hosted on [Exercism.io](https://exercism.io/). You can view my profile [here](https://exercism.io/profiles/Xevion). The reason for this is simply to have a place where I can collect my comments, as well as just have some fun with Python and webscraping. Exercise file and exercise submission links will be provided for each and every exercise. -This file is for the **Python** track, contains **48** submissions, **18** of which have comments. This file was built on **24-07-2019** at **04:30:19 UTC**. +This file is for the **Python** track, contains **53** submissions, **21** of which have comments. This file was built on **24-07-2019** at **20:44:28 UTC**. ## Word Count @@ -206,12 +206,30 @@ I don't like this solution since the measurements are off and they don't provide The pytester looks wrong to me. I tested mine online and double checked that it's doing it correctly, and the solutions on the Instructions page look right, yet mine do not solve test correctly through the pytester. I dunno, it feels weird for it to be wrong for such a simple problem. +## ETL + +[Link to File](./etl/etl.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/etl/solutions/0b06a001a17647c89efc9e975b61f648) + +To this day, the way dictionary and list comprehension works, as in, the order you have to put it, with the nested unpacking (`for value in values`), it messes with me still to this day. I'm just glad it works, because it's simply amazing. + +## Prime Factors + +[Link to File](./prime-factors/prime_factors.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/prime-factors/solutions/8594f50d455a42c2bcaaf99a23da57ec) + + + ## Scrabble Score [Link to File](./scrabble-score/scrabble_score.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/scrabble-score/solutions/ee423be717314687b974302d5cc82503) +## Sublist + +[Link to File](./sublist/sublist.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/sublist/solutions/a217429e9ed74c7f8d0e72bb6660d881) + +50th exercise in the Python Track. Not exactly my favorite exercise, finding sublists in lists. How fun. How exciting. How empowering. + ## Robot Name [Link to File](./robot-name/robot_name.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/robot-name/solutions/f2053e5f37aa4e7594658ff52cd743a7) @@ -276,6 +294,18 @@ Truly cursed. +## Food Chain + +[Link to File](./food-chain/food_chain.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/food-chain/solutions/b890dc4d044149a7bfbd854912c68bbf) + + + +## Spiral Matrix + +[Link to File](./spiral-matrix/spiral_matrix.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/spiral-matrix/solutions/e351192377fc41329076c9d6636ef233) + +There is probably a better, mathematical way of doing this, maybe using recursion, but I'm really bad with numbers, so I just went with an easier but probably slower way to do it. + ## Raindrops [Link to File](./raindrops/raindrops.py) | [Link to Submission](https://exercism.io/tracks/python/exercises/raindrops/solutions/95285ff036d04de1a103805ed7145f20) diff --git a/python/food-chain/food_chain.py b/python/food-chain/food_chain.py index c739e9b..19cfae1 100644 --- a/python/food-chain/food_chain.py +++ b/python/food-chain/food_chain.py @@ -39,15 +39,18 @@ def construct(verse): if versen == 2: return middle.format(names[versen], names[versen-1] + ' ' + special_middle) return middle.format(names[versen], names[versen-1]) + # Horse verse if verse == 7: return "{}\n{}".format(intro.format(names[verse]), last) + # Constructs the intoduction of a verse constructed_intro = '{}{}'.format( intro.format(names[verse]), '\n' + intro_addendums[verse-1] if verse > 0 else '' ) - # Constructs the middle verse using make middle + constructed_middle = '\n'.join([''] + [makemiddle(versen) for versen in range(verse, 0, -1)]) if verse >= 1 else '' - # Finally constructs the entire properly. + + # Finally constructs the entire verse properly. return "{}{}{}".format(constructed_intro, constructed_middle, '\n' + outro) \ No newline at end of file diff --git a/python/ledger/.exercism/metadata.json b/python/ledger/.exercism/metadata.json new file mode 100644 index 0000000..96155d4 --- /dev/null +++ b/python/ledger/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"ledger","id":"e2e05dcdf02f44bdbf5f727a551742b5","url":"https://exercism.io/my/solutions/e2e05dcdf02f44bdbf5f727a551742b5","handle":"Xevion","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/ledger/README.md b/python/ledger/README.md new file mode 100644 index 0000000..d041e56 --- /dev/null +++ b/python/ledger/README.md @@ -0,0 +1,60 @@ +# Ledger + +Refactor a ledger printer. + +The ledger exercise is a refactoring exercise. There is code that prints a +nicely formatted ledger, given a locale (American or Dutch) and a currency (US +dollar or euro). The code however is rather badly written, though (somewhat +surprisingly) it consistently passes the test suite. + +Rewrite this code. Remember that in refactoring the trick is to make small steps +that keep the tests passing. That way you can always quickly go back to a +working version. Version control tools like git can help here as well. + +Please keep a log of what changes you've made and make a comment on the exercise +containing that log, this will help reviewers. + +## 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 ledger_test.py` +- Python 3.4+: `pytest ledger_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 ledger_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/ledger` 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). + +## 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/ledger/ledger.py b/python/ledger/ledger.py new file mode 100644 index 0000000..736af60 --- /dev/null +++ b/python/ledger/ledger.py @@ -0,0 +1,296 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + + +class LedgerEntry(object): + def __init__(self, date, description, change): + self.date, self.description, self.change = date, description, change + + +def create_entry(date, description, change): + return LedgerEntry(datetime.strptime(date, '%Y-%m-%d'), description, change) + + +def format_entries(currency, locale, entries): + if locale == 'en_US': + # Generate Header Row + table = 'Date' + for _ in range(7): + table += ' ' + table += '| Description' + for _ in range(15): + table += ' ' + table += '| Change' + for _ in range(7): + table += ' ' + + while len(entries) > 0: + table += '\n' + + # Find next entry in order + min_entry_index = -1 + for i in range(len(entries)): + entry = entries[i] + if min_entry_index < 0: + min_entry_index = i + continue + min_entry = entries[min_entry_index] + if entry.date < min_entry.date: + min_entry_index = i + continue + if ( + entry.date == min_entry.date and + entry.change < min_entry.change + ): + min_entry_index = i + continue + if ( + entry.date == min_entry.date and + entry.change == min_entry.change and + entry.description < min_entry.description + ): + min_entry_index = i + continue + entry = entries[min_entry_index] + entries.pop(min_entry_index) + + # Write entry date to table + month = entry.date.month + month = str(month) + + if len(month) < 2: + month = '0' + month + date_str = month + date_str += '/' + day = entry.date.day + day = str(day) + + if len(day) < 2: + day = '0' + day + date_str += day + date_str += '/' + year = entry.date.year + year = str(year) + + while len(year) < 4: + year = '0' + year + date_str += year + table += date_str + table += ' | ' + + # Write entry description to table + # Truncate if necessary + if len(entry.description) > 25: + for i in range(22): + table += entry.description[i] + table += '...' + else: + for i in range(25): + if len(entry.description) > i: + table += entry.description[i] + else: + table += ' ' + table += ' | ' + + # Write entry change to table + if currency == 'USD': + change_str = '' + if entry.change < 0: + change_str = '(' + change_str += '$' + change_dollar = abs(int(entry.change / 100.0)) + dollar_parts = [] + while change_dollar > 0: + dollar_parts.insert(0, str(change_dollar % 1000)) + change_dollar = change_dollar // 1000 + if len(dollar_parts) == 0: + change_str += '0' + else: + while True: + change_str += dollar_parts[0] + dollar_parts.pop(0) + if len(dollar_parts) == 0: + break + change_str += ',' + change_str += '.' + change_cents = abs(entry.change) % 100 + change_cents = str(change_cents) + if len(change_cents) < 2: + change_cents = '0' + change_cents + change_str += change_cents + if entry.change < 0: + change_str += ')' + else: + change_str += ' ' + while len(change_str) < 13: + change_str = ' ' + change_str + table += change_str + elif currency == 'EUR': + change_str = '' + if entry.change < 0: + change_str = '(' + change_str += u'€' + change_euro = abs(int(entry.change / 100.0)) + euro_parts = [] + while change_euro > 0: + euro_parts.insert(0, str(change_euro % 1000)) + change_euro = change_euro // 1000 + if len(euro_parts) == 0: + change_str += '0' + else: + while True: + change_str += euro_parts[0] + euro_parts.pop(0) + if len(euro_parts) == 0: + break + change_str += ',' + change_str += '.' + change_cents = abs(entry.change) % 100 + change_cents = str(change_cents) + if len(change_cents) < 2: + change_cents = '0' + change_cents + change_str += change_cents + if entry.change < 0: + change_str += ')' + else: + change_str += ' ' + while len(change_str) < 13: + change_str = ' ' + change_str + table += change_str + return table + + elif locale == 'nl_NL': + # Generate Header Row + table = 'Datum' + for _ in range(6): + table += ' ' + table += '| Omschrijving' + for _ in range(14): + table += ' ' + table += '| Verandering' + for _ in range(2): + table += ' ' + + while len(entries) > 0: + table += '\n' + + # Find next entry in order + min_entry_index = -1 + for i in range(len(entries)): + entry = entries[i] + if min_entry_index < 0: + min_entry_index = i + continue + min_entry = entries[min_entry_index] + if entry.date < min_entry.date: + min_entry_index = i + continue + if ( + entry.date == min_entry.date and + entry.change < min_entry.change + ): + min_entry_index = i + continue + if ( + entry.date == min_entry.date and + entry.change == min_entry.change and + entry.description < min_entry.description + ): + min_entry_index = i + continue + entry = entries[min_entry_index] + entries.pop(min_entry_index) + + # Write entry date to table + day = entry.date.day + day = str(day) + if len(day) < 2: + day = '0' + day + date_str = day + date_str += '-' + month = entry.date.month + month = str(month) + if len(month) < 2: + month = '0' + month + date_str += month + date_str += '-' + year = entry.date.year + year = str(year) + while len(year) < 4: + year = '0' + year + date_str += year + table += date_str + table += ' | ' + + # Write entry description to table + # Truncate if necessary + if len(entry.description) > 25: + for i in range(22): + table += entry.description[i] + table += '...' + else: + for i in range(25): + if len(entry.description) > i: + table += entry.description[i] + else: + table += ' ' + table += ' | ' + + # Write entry change to table + if currency == 'USD': + change_str = '$ ' + if entry.change < 0: + change_str += '-' + change_dollar = abs(int(entry.change / 100.0)) + dollar_parts = [] + while change_dollar > 0: + dollar_parts.insert(0, str(change_dollar % 1000)) + change_dollar = change_dollar // 1000 + if len(dollar_parts) == 0: + change_str += '0' + else: + while True: + change_str += dollar_parts[0] + dollar_parts.pop(0) + if len(dollar_parts) == 0: + break + change_str += '.' + change_str += ',' + change_cents = abs(entry.change) % 100 + change_cents = str(change_cents) + if len(change_cents) < 2: + change_cents = '0' + change_cents + change_str += change_cents + change_str += ' ' + while len(change_str) < 13: + change_str = ' ' + change_str + table += change_str + elif currency == 'EUR': + change_str = u'€ ' + if entry.change < 0: + change_str += '-' + change_euro = abs(int(entry.change / 100.0)) + euro_parts = [] + while change_euro > 0: + euro_parts.insert(0, str(change_euro % 1000)) + change_euro = change_euro // 1000 + if len(euro_parts) == 0: + change_str += '0' + else: + while True: + change_str += euro_parts[0] + euro_parts.pop(0) + if len(euro_parts) == 0: + break + change_str += '.' + change_str += ',' + change_cents = abs(entry.change) % 100 + change_cents = str(change_cents) + if len(change_cents) < 2: + change_cents = '0' + change_cents + change_str += change_cents + change_str += ' ' + while len(change_str) < 13: + change_str = ' ' + change_str + table += change_str + return table diff --git a/python/ledger/ledger_test.py b/python/ledger/ledger_test.py new file mode 100644 index 0000000..f0de049 --- /dev/null +++ b/python/ledger/ledger_test.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +import unittest + +from ledger import format_entries, create_entry + + +class LedgerTest(unittest.TestCase): + maxDiff = 5000 + + def test_empty_ledger(self): + currency = 'USD' + locale = 'en_US' + entries = [] + expected = 'Date | Description | Change ' + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_one_entry(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-01-01', 'Buy present', -1000), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_credit_and_debit(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-01-02', 'Get present', 1000), + create_entry('2015-01-01', 'Buy present', -1000), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + '01/02/2015 | Get present | $10.00 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_multiple_entries_on_same_date_ordered_by_description(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-01-02', 'Get present', 1000), + create_entry('2015-01-01', 'Buy present', -1000), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '01/01/2015 | Buy present | ($10.00)', + '01/02/2015 | Get present | $10.00 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_final_order_tie_breaker_is_change(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-01-01', 'Something', 0), + create_entry('2015-01-01', 'Something', -1), + create_entry('2015-01-01', 'Something', 1), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '01/01/2015 | Something | ($0.01)', + '01/01/2015 | Something | $0.00 ', + '01/01/2015 | Something | $0.01 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_overlong_description(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-01-01', 'Freude schoner Gotterfunken', -123456), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '01/01/2015 | Freude schoner Gotterf... | ($1,234.56)', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_euros(self): + currency = 'EUR' + locale = 'en_US' + entries = [ + create_entry('2015-01-01', 'Buy present', -1000), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + u'01/01/2015 | Buy present | (€10.00)', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_dutch_locale(self): + currency = 'USD' + locale = 'nl_NL' + entries = [ + create_entry('2015-03-12', 'Buy present', 123456), + ] + expected = '\n'.join([ + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ 1.234,56 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_dutch_locale_and_euros(self): + currency = 'EUR' + locale = 'nl_NL' + entries = [ + create_entry('2015-03-12', 'Buy present', 123456), + ] + expected = '\n'.join([ + 'Datum | Omschrijving | Verandering ', + u'12-03-2015 | Buy present | € 1.234,56 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_dutch_negative_number_with_3_digits_before_decimal_point(self): + currency = 'USD' + locale = 'nl_NL' + entries = [ + create_entry('2015-03-12', 'Buy present', -12345), + ] + expected = '\n'.join([ + 'Datum | Omschrijving | Verandering ', + '12-03-2015 | Buy present | $ -123,45 ', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + def test_american_negative_number_with_3_digits_before_decimal_point(self): + currency = 'USD' + locale = 'en_US' + entries = [ + create_entry('2015-03-12', 'Buy present', -12345), + ] + expected = '\n'.join([ + 'Date | Description | Change ', + '03/12/2015 | Buy present | ($123.45)', + ]) + self.assertEqual(format_entries(currency, locale, entries), expected) + + +if __name__ == '__main__': + unittest.main()