diff --git a/python/README.md b/python/README.md index 0f7c8778..74d350ab 100644 --- a/python/README.md +++ b/python/README.md @@ -44,3 +44,4 @@ - [roman-numerals](./roman-numerals/README.md) - [scrabble-score](./scrabble-score/README.md) - [difference-of-squares](./difference-of-squares/README.md) +- [luhn](./luhn/README.md) diff --git a/python/luhn/.coverage b/python/luhn/.coverage new file mode 100644 index 00000000..bbbab86d --- /dev/null +++ b/python/luhn/.coverage @@ -0,0 +1 @@ +!coverage.py: This is a private format, don't read it directly!{"arcs":{"/home/vpayno/git_vpayno/exercism-workspace/python/luhn/test/__init__.py":[[0,0],[0,-1]],"/home/vpayno/git_vpayno/exercism-workspace/python/luhn/luhn.py":[[0,1],[1,3],[3,4],[4,7],[7,7],[7,8],[8,10],[10,27],[27,-7],[7,55],[55,55],[55,56],[56,58],[58,61],[61,95],[95,137],[137,-55],[55,-1],[58,59],[59,-58],[61,87],[87,88],[88,89],[10,25],[25,-10],[89,93],[95,129],[27,37],[37,50],[50,52],[37,48],[48,-37],[52,52],[52,-27],[129,131],[137,175],[175,177],[177,178],[178,180],[180,182],[182,183],[183,185],[185,186],[186,188],[188,182],[182,191],[191,193],[193,195],[195,198],[198,-137],[131,133],[133,135],[135,-95],[93,-61],[195,196],[196,198],[87,91],[91,-61],[185,188],[89,91]]}} \ No newline at end of file diff --git a/python/luhn/.coverage.xml b/python/luhn/.coverage.xml new file mode 100644 index 00000000..5e1c593b --- /dev/null +++ b/python/luhn/.coverage.xml @@ -0,0 +1,65 @@ + + + + + + /home/vpayno/git_vpayno/exercism-workspace/python/luhn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/luhn/.coveragerc b/python/luhn/.coveragerc new file mode 100644 index 00000000..883531db --- /dev/null +++ b/python/luhn/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = __init__.py, *_test.py diff --git a/python/luhn/.exercism/config.json b/python/luhn/.exercism/config.json new file mode 100644 index 00000000..c9a7c188 --- /dev/null +++ b/python/luhn/.exercism/config.json @@ -0,0 +1,39 @@ +{ + "authors": [ + "sjakobi" + ], + "contributors": [ + "AnAccountForReportingBugs", + "behrtam", + "BethanyG", + "cmccandless", + "Dog", + "Grociu", + "ikhadykin", + "kytrinyx", + "mambocab", + "N-Parsons", + "Nishant23", + "olufotebig", + "pheanex", + "rootulp", + "thomasjpfan", + "tqa236", + "xitanggg", + "yawpitch" + ], + "files": { + "solution": [ + "luhn.py" + ], + "test": [ + "luhn_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Given a number determine whether or not it is valid per the Luhn formula.", + "source": "The Luhn Algorithm on Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Luhn_algorithm" +} diff --git a/python/luhn/.exercism/metadata.json b/python/luhn/.exercism/metadata.json new file mode 100644 index 00000000..79ee9904 --- /dev/null +++ b/python/luhn/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"luhn","id":"fda87fb1d5324fef81e9bfc025d2666e","url":"https://exercism.org/tracks/python/exercises/luhn","handle":"vpayno","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/luhn/.pylintrc b/python/luhn/.pylintrc new file mode 120000 index 00000000..30b33b52 --- /dev/null +++ b/python/luhn/.pylintrc @@ -0,0 +1 @@ +../.pylintrc \ No newline at end of file diff --git a/python/luhn/HELP.md b/python/luhn/HELP.md new file mode 100644 index 00000000..53cea3f1 --- /dev/null +++ b/python/luhn/HELP.md @@ -0,0 +1,130 @@ +# Help + +## Running the tests + +We use [pytest][pytest: Getting Started Guide] as our website test runner. +You will need to install `pytest` on your development machine if you want to run tests for the Python track locally. +You should also install the following `pytest` plugins: + +- [pytest-cache][pytest-cache] +- [pytest-subtests][pytest-subtests] + +Extended information can be found in our website [Python testing guide][Python track tests page]. + + +### Running Tests + +To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_). +Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded. + +Linux/MacOS +```bash +$ cd {path/to/exercise-folder-location} +``` + +Windows +```powershell +PS C:\Users\foobar> cd {path\to\exercise-folder-location} +``` + +
+ +Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file: + +Linux/MacOS +```bash +$ python3 -m pytest -o markers=task {exercise_test.py} +==================== 7 passed in 0.08s ==================== +``` + +Windows +```powershell +PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py} +==================== 7 passed in 0.08s ==================== +``` + + +### Common options +- `-o` : override default `pytest.ini` (_you can use this to avoid marker warnings_) +- `-v` : enable verbose output. +- `-x` : stop running tests on first failure. +- `--ff` : run failures from previous test before running other test cases. + +For additional options, use `python3 -m pytest -h` or `py -m pytest -h`. + + +### Fixing warnings + +If you do not use `pytest -o markers=task` when invoking `pytest`, you might receive a `PytestUnknownMarkWarning` for tests that use our new syntax: + +```bash +PytestUnknownMarkWarning: Unknown pytest.mark.task - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html +``` + +To avoid typing `pytest -o markers=task` for every test you run, you can use a `pytest.ini` configuration file. +We have made one that can be downloaded from the top level of the Python track directory: [pytest.ini][pytest.ini]. + +You can also create your own `pytest.ini` file with the following content: + +```ini +[pytest] +markers = + task: A concept exercise task. +``` + +Placing the `pytest.ini` file in the _root_ or _working_ directory for your Python track exercises will register the marks and stop the warnings. +More information on pytest marks can be found in the `pytest` documentation on [marking test functions][pytest: marking test functions with attributes] and the `pytest` documentation on [working with custom markers][pytest: working with custom markers]. + +Information on customizing pytest configurations can be found in the `pytest` documentation on [configuration file formats][pytest: configuration file formats]. + + +### Extending your IDE or Code Editor + +Many IDEs and code editors have built-in support for using `pytest` and other code quality tools. +Some community-sourced options can be found on our [Python track tools page][Python track tools page]. + +[Pytest: Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html +[Python track tools page]: https://exercism.org/docs/tracks/python/tools +[Python track tests page]: https://exercism.org/docs/tracks/python/tests +[pytest-cache]:http://pythonhosted.org/pytest-cache/ +[pytest-subtests]:https://github.com/pytest-dev/pytest-subtests +[pytest.ini]: https://github.com/exercism/python/blob/main/pytest.ini +[pytest: configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats +[pytest: marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks +[pytest: working with custom markers]: https://docs.pytest.org/en/6.2.x/example/markers.html#working-with-custom-markers + +## Submitting your solution + +You can submit your solution using the `exercism submit luhn.py` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Python track's documentation](https://exercism.org/docs/tracks/python) +- The [Python track's programming category on the forum](https://forum.exercism.org/c/programming/python) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +Below are some resources for getting help if you run into trouble: + +- [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources. +- [The Exercism Community on Discord](https://exercism.org/r/discord) +- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community. +- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners. +- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done. +- [Python Community Forums](https://discuss.python.org/) +- [Free Code Camp Community Forums](https://forum.freecodecamp.org/) +- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help) +- [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually. + +Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already. + If not - you can always [ask](https://stackoverflow.com/help/how-to-ask) or [answer](https://stackoverflow.com/help/how-to-answer) someone else's question. \ No newline at end of file diff --git a/python/luhn/README.md b/python/luhn/README.md new file mode 100644 index 00000000..df557c5b --- /dev/null +++ b/python/luhn/README.md @@ -0,0 +1,105 @@ +# Luhn + +Welcome to Luhn on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Instructions + +Given a number determine whether or not it is valid per the Luhn formula. + +The [Luhn algorithm][luhn] is a simple checksum formula used to validate a variety of identification numbers, such as credit card numbers and Canadian Social Insurance Numbers. + +The task is to check if a given string is valid. + +## Validating a Number + +Strings of length 1 or less are not valid. +Spaces are allowed in the input, but they should be stripped before checking. +All other non-digit characters are disallowed. + +### Example 1: valid credit card number + +```text +4539 3195 0343 6467 +``` + +The first step of the Luhn algorithm is to double every second digit, starting from the right. +We will be doubling + +```text +4_3_ 3_9_ 0_4_ 6_6_ +``` + +If doubling the number results in a number greater than 9 then subtract 9 from the product. +The results of our doubling: + +```text +8569 6195 0383 3437 +``` + +Then sum all of the digits: + +```text +8+5+6+9+6+1+9+5+0+3+8+3+3+4+3+7 = 80 +``` + +If the sum is evenly divisible by 10, then the number is valid. +This number is valid! + +### Example 2: invalid credit card number + +```text +8273 1232 7352 0569 +``` + +Double the second digits, starting from the right + +```text +7253 2262 5312 0539 +``` + +Sum the digits + +```text +7+2+5+3+2+2+6+2+5+3+1+2+0+5+3+9 = 57 +``` + +57 is not evenly divisible by 10, so this number is not valid. + +[luhn]: https://en.wikipedia.org/wiki/Luhn_algorithm + +## Source + +### Created by + +- @sjakobi + +### Contributed to by + +- @AnAccountForReportingBugs +- @behrtam +- @BethanyG +- @cmccandless +- @Dog +- @Grociu +- @ikhadykin +- @kytrinyx +- @mambocab +- @N-Parsons +- @Nishant23 +- @olufotebig +- @pheanex +- @rootulp +- @thomasjpfan +- @tqa236 +- @xitanggg +- @yawpitch + +### Based on + +The Luhn Algorithm on Wikipedia - https://en.wikipedia.org/wiki/Luhn_algorithm + +### My Solution + +- [my solution](./luhn.py) +- [run-tests](./run-tests-python.txt) diff --git a/python/luhn/luhn.py b/python/luhn/luhn.py new file mode 100644 index 00000000..185e9f6d --- /dev/null +++ b/python/luhn/luhn.py @@ -0,0 +1,198 @@ +"""Python Luhn Exercism""" + +import re +from itertools import chain + + +class String(str): + """Extending String class/type""" + + def match(self, regex: str) -> bool: + """Does the String match a regex? + + >>> s: String = String("1234 5678") + >>> s + '1234 5678' + >>> s.match(r"^[0-9 ]+$") + True + >>> s.match(r"^[a-z ]+$") + False + + :param regex: rstr + :return: bool + """ + + return bool(re.match(regex, self)) + + def extract_digits(self) -> list[int]: + """Extracts digits from a code. + + >>> s: String = String("1234 5678") + >>> s.extract_digits() + [1, 2, 3, 4, 5, 6, 7, 8] + + :return: list[int] + """ + + def is_digit(rune: str) -> bool: + """Test function for filter. + + >>> is_digit("a") + False + >>> is_digit("7") + True + + :return: True if rune is a digit + """ + + return rune.isdigit() + + filtered = filter(is_digit, list(self)) + + return [int(r) for r in filtered] + + +class Luhn: # pylint: disable=too-few-public-methods + """Determine if a number is a valid Luhn number.""" + + def __init__(self, card_num: str) -> None: + self.code: String = String(card_num.strip()) + + def valid(self) -> bool: + """Is this a valid Luhn number? + + >>> l: Luhn = Luhn("055 444 285") + >>> l.valid() + True + >>> l: Luhn = Luhn("055 444 286") + >>> l.valid() + False + >>> l: Luhn = Luhn("234 567 891 234") + >>> l.valid() + True + >>> n: String = String("234 567 891 234") + >>> n == "0" + False + >>> len(n) == 0 + False + >>> n == "0" + False + >>> n.match(r"^([0-9 ])+$") + True + + :return: bool + """ + + if ( + self.code == "0" + or len(self.code) == 0 + or not self.code.match(r"^([0-9 ])+$") + ): + return False + + return self.__is_luhn_number() + + def __is_luhn_number(self) -> bool: + """Is the code a valid luhn number? + + >>> l: Luhn = Luhn("055 444 285") + >>> l._Luhn__is_luhn_number() + True + >>> l: Luhn = Luhn("055 444 286") + >>> l._Luhn__is_luhn_number() + False + >>> digits: list[int] = l.code.extract_digits() + >>> digits + [0, 5, 5, 4, 4, 4, 2, 8, 6] + >>> numbers: list[int] = l._Luhn__step_one_and_two(digits) + >>> numbers + [1, 5, 8, 4, 8, 2, 7, 6, 0] + >>> digit_sum: int = sum(numbers) + >>> digit_sum + 41 + >>> l: Luhn = Luhn("234 567 891 234") + >>> l._Luhn__is_luhn_number() + True + >>> digits: list[int] = l.code.extract_digits() + >>> digits + [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] + >>> numbers: list[int] = l._Luhn__step_one_and_two(digits) + >>> numbers + [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] + >>> digit_sum: int = sum(numbers) + >>> digit_sum + 60 + + :return: bool + """ + + digits: list[int] = self.code.extract_digits() + + numbers: list[int] = self.__step_one_and_two(digits) + + digit_sum: int = sum(numbers) + + return (digit_sum % 10) == 0 + + def __step_one_and_two(self, digits: list[int]) -> list[int]: + """Performs steps one and two of the luhn number verification algorithm. + + >>> l: Luhn = Luhn("0123456789") + >>> d: list[int] = l.code.extract_digits() + >>> d + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + >>> l._Luhn__step_one_and_two(d) + [0, 1, 4, 3, 8, 5, 3, 7, 7, 9] + >>> l: Luhn = Luhn("59") + >>> d: list[int] = l.code.extract_digits() + >>> d + [5, 9] + >>> s = l._Luhn__step_one_and_two(d) + >>> s + [1, 9] + >>> sum(s) + 10 + >>> l: Luhn = Luhn(" 0") + >>> d: list[int] = l.code.extract_digits() + >>> d + [0] + >>> l._Luhn__step_one_and_two(d) + [0] + >>> sum(l._Luhn__step_one_and_two(d)) + 0 + >>> l: Luhn = Luhn("234 567 891 234") + >>> d: list[int] = l.code.extract_digits() + >>> d + [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] + >>> l._Luhn__step_one_and_two(d) + [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] + >>> sum(l._Luhn__step_one_and_two(d)) + 57 + + :return: list[int] + """ + + digits.reverse() + + even: list[int] = digits[0::2] + odd: list[int] = digits[1::2] + + new_odd: list[int] = [] + + for n in odd: + n *= 2 + + if n > 9: + n -= 9 + + new_odd.append(n) + + # flatten the list of tuples + new_list: list[int] = list(chain.from_iterable(zip(even, new_odd))) + + new_list.reverse() + + if len(even) != len(odd): + new_list.append(even[-1]) + + return new_list diff --git a/python/luhn/luhn.py,cover b/python/luhn/luhn.py,cover new file mode 100644 index 00000000..dda3ede6 --- /dev/null +++ b/python/luhn/luhn.py,cover @@ -0,0 +1,198 @@ +> """Python Luhn Exercism""" + +> import re +> from itertools import chain + + +> class String(str): +> """Extending String class/type""" + +> def match(self, regex: str) -> bool: +> """Does the String match a regex? + +> >>> s: String = String("1234 5678") +> >>> s +> '1234 5678' +> >>> s.match(r"^[0-9 ]+$") +> True +> >>> s.match(r"^[a-z ]+$") +> False + +> :param regex: rstr +> :return: bool +> """ + +> return bool(re.match(regex, self)) + +> def extract_digits(self) -> list[int]: +> """Extracts digits from a code. + +> >>> s: String = String("1234 5678") +> >>> s.extract_digits() +> [1, 2, 3, 4, 5, 6, 7, 8] + +> :return: list[int] +> """ + +> def is_digit(rune: str) -> bool: +> """Test function for filter. + +> >>> is_digit("a") +> False +> >>> is_digit("7") +> True + +> :return: True if rune is a digit +> """ + +> return rune.isdigit() + +> filtered = filter(is_digit, list(self)) + +> return [int(r) for r in filtered] + + +> class Luhn: # pylint: disable=too-few-public-methods +> """Determine if a number is a valid Luhn number.""" + +> def __init__(self, card_num: str) -> None: +> self.code: String = String(card_num.strip()) + +> def valid(self) -> bool: +> """Is this a valid Luhn number? + +> >>> l: Luhn = Luhn("055 444 285") +> >>> l.valid() +> True +> >>> l: Luhn = Luhn("055 444 286") +> >>> l.valid() +> False +> >>> l: Luhn = Luhn("234 567 891 234") +> >>> l.valid() +> True +> >>> n: String = String("234 567 891 234") +> >>> n == "0" +> False +> >>> len(n) == 0 +> False +> >>> n == "0" +> False +> >>> n.match(r"^([0-9 ])+$") +> True + +> :return: bool +> """ + +> if ( +> self.code == "0" +> or len(self.code) == 0 +> or not self.code.match(r"^([0-9 ])+$") +> ): +> return False + +> return self.__is_luhn_number() + +> def __is_luhn_number(self) -> bool: +> """Is the code a valid luhn number? + +> >>> l: Luhn = Luhn("055 444 285") +> >>> l._Luhn__is_luhn_number() +> True +> >>> l: Luhn = Luhn("055 444 286") +> >>> l._Luhn__is_luhn_number() +> False +> >>> digits: list[int] = l.code.extract_digits() +> >>> digits +> [0, 5, 5, 4, 4, 4, 2, 8, 6] +> >>> numbers: list[int] = l._Luhn__step_one_and_two(digits) +> >>> numbers +> [1, 5, 8, 4, 8, 2, 7, 6, 0] +> >>> digit_sum: int = sum(numbers) +> >>> digit_sum +> 41 +> >>> l: Luhn = Luhn("234 567 891 234") +> >>> l._Luhn__is_luhn_number() +> True +> >>> digits: list[int] = l.code.extract_digits() +> >>> digits +> [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] +> >>> numbers: list[int] = l._Luhn__step_one_and_two(digits) +> >>> numbers +> [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] +> >>> digit_sum: int = sum(numbers) +> >>> digit_sum +> 60 + +> :return: bool +> """ + +> digits: list[int] = self.code.extract_digits() + +> numbers: list[int] = self.__step_one_and_two(digits) + +> digit_sum: int = sum(numbers) + +> return (digit_sum % 10) == 0 + +> def __step_one_and_two(self, digits: list[int]) -> list[int]: +> """Performs steps one and two of the luhn number verification algorithm. + +> >>> l: Luhn = Luhn("0123456789") +> >>> d: list[int] = l.code.extract_digits() +> >>> d +> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +> >>> l._Luhn__step_one_and_two(d) +> [0, 1, 4, 3, 8, 5, 3, 7, 7, 9] +> >>> l: Luhn = Luhn("59") +> >>> d: list[int] = l.code.extract_digits() +> >>> d +> [5, 9] +> >>> s = l._Luhn__step_one_and_two(d) +> >>> s +> [1, 9] +> >>> sum(s) +> 10 +> >>> l: Luhn = Luhn(" 0") +> >>> d: list[int] = l.code.extract_digits() +> >>> d +> [0] +> >>> l._Luhn__step_one_and_two(d) +> [0] +> >>> sum(l._Luhn__step_one_and_two(d)) +> 0 +> >>> l: Luhn = Luhn("234 567 891 234") +> >>> d: list[int] = l.code.extract_digits() +> >>> d +> [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] +> >>> l._Luhn__step_one_and_two(d) +> [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] +> >>> sum(l._Luhn__step_one_and_two(d)) +> 57 + +> :return: list[int] +> """ + +> digits.reverse() + +> even: list[int] = digits[0::2] +> odd: list[int] = digits[1::2] + +> new_odd: list[int] = [] + +> for n in odd: +> n *= 2 + +> if n > 9: +> n -= 9 + +> new_odd.append(n) + + # flatten the list of tuples +> new_list: list[int] = list(chain.from_iterable(zip(even, new_odd))) + +> new_list.reverse() + +> if len(even) != len(odd): +> new_list.append(even[-1]) + +> return new_list diff --git a/python/luhn/luhn_test.py b/python/luhn/luhn_test.py new file mode 100644 index 00000000..58234eb7 --- /dev/null +++ b/python/luhn/luhn_test.py @@ -0,0 +1,89 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/luhn/canonical-data.json +# File last updated on 2023-07-19 + +import unittest + +from luhn import ( + Luhn, +) + + +class LuhnTest(unittest.TestCase): + def test_single_digit_strings_can_not_be_valid(self): + self.assertIs(Luhn("1").valid(), False) + + def test_a_single_zero_is_invalid(self): + self.assertIs(Luhn("0").valid(), False) + + def test_a_simple_valid_sin_that_remains_valid_if_reversed(self): + self.assertIs(Luhn("059").valid(), True) + + def test_a_simple_valid_sin_that_becomes_invalid_if_reversed(self): + self.assertIs(Luhn("59").valid(), True) + + def test_a_valid_canadian_sin(self): + self.assertIs(Luhn("055 444 285").valid(), True) + + def test_invalid_canadian_sin(self): + self.assertIs(Luhn("055 444 286").valid(), False) + + def test_invalid_credit_card(self): + self.assertIs(Luhn("8273 1232 7352 0569").valid(), False) + + def test_invalid_long_number_with_an_even_remainder(self): + self.assertIs(Luhn("1 2345 6789 1234 5678 9012").valid(), False) + + def test_invalid_long_number_with_a_remainder_divisible_by_5(self): + self.assertIs(Luhn("1 2345 6789 1234 5678 9013").valid(), False) + + def test_valid_number_with_an_even_number_of_digits(self): + self.assertIs(Luhn("095 245 88").valid(), True) + + def test_valid_number_with_an_odd_number_of_spaces(self): + self.assertIs(Luhn("234 567 891 234").valid(), True) + + def test_valid_strings_with_a_non_digit_added_at_the_end_become_invalid(self): + self.assertIs(Luhn("059a").valid(), False) + + def test_valid_strings_with_punctuation_included_become_invalid(self): + self.assertIs(Luhn("055-444-285").valid(), False) + + def test_valid_strings_with_symbols_included_become_invalid(self): + self.assertIs(Luhn("055# 444$ 285").valid(), False) + + def test_single_zero_with_space_is_invalid(self): + self.assertIs(Luhn(" 0").valid(), False) + + def test_more_than_a_single_zero_is_valid(self): + self.assertIs(Luhn("0000 0").valid(), True) + + def test_input_digit_9_is_correctly_converted_to_output_digit_9(self): + self.assertIs(Luhn("091").valid(), True) + + def test_very_long_input_is_valid(self): + self.assertIs(Luhn("9999999999 9999999999 9999999999 9999999999").valid(), True) + + def test_valid_luhn_with_an_odd_number_of_digits_and_non_zero_first_digit(self): + self.assertIs(Luhn("109").valid(), True) + + def test_using_ascii_value_for_non_doubled_non_digit_isn_t_allowed(self): + self.assertIs(Luhn("055b 444 285").valid(), False) + + def test_using_ascii_value_for_doubled_non_digit_isn_t_allowed(self): + self.assertIs(Luhn(":9").valid(), False) + + def test_non_numeric_non_space_char_in_the_middle_with_a_sum_that_s_divisible_by_10_isn_t_allowed( + self, + ): + self.assertIs(Luhn("59%59").valid(), False) + + # Additional tests for this track + + def test_is_valid_can_be_called_repeatedly(self): + # This test was added, because we saw many implementations + # in which the first call to valid() worked, but the + # second call failed(). + number = Luhn("055 444 285") + self.assertIs(number.valid(), True) + self.assertIs(number.valid(), True) diff --git a/python/luhn/pytest.ini b/python/luhn/pytest.ini new file mode 100644 index 00000000..8dc12a49 --- /dev/null +++ b/python/luhn/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +pythonpath = . +addopts = --doctest-modules +markers = + task: exercise task/step diff --git a/python/luhn/run-tests-python.txt b/python/luhn/run-tests-python.txt new file mode 100644 index 00000000..d024b267 --- /dev/null +++ b/python/luhn/run-tests-python.txt @@ -0,0 +1,764 @@ +Running automated test file(s): + + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-pylint + +Running Python Lint - PyLint + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: pylint --version + +pylint 3.0.3 +astroid 3.0.2 +Python 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110] + +real 0m0.231s +user 0m0.162s +sys 0m0.071s + + + ============================================================================== + +Running: pylint ./src + + +-------------------------------------------------------------------- +Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00) + + +real 0m0.624s +user 0m0.547s +sys 0m0.080s + + + ============================================================================== + +Exit code: 0 + +real 0m1.805s +user 0m1.361s +sys 0m0.469s + +real 0m1.809s +user 0m1.364s +sys 0m0.470s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-ruff + +Running Python Lint - Ruff + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: ruff --version + +ruff 0.3.5 + +real 0m0.083s +user 0m0.039s +sys 0m0.048s + + + ============================================================================== + +Running: ruff check --ignore E501 ./src + +All checks passed! + +real 0m0.114s +user 0m0.045s +sys 0m0.073s + + + ============================================================================== + +Exit code: 0 + +real 0m1.107s +user 0m0.738s +sys 0m0.392s + +real 0m1.109s +user 0m0.738s +sys 0m0.395s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-pyright + +Running Python Lint - PyRight + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: pyright --version + +pyright 1.1.359 + +real 0m0.880s +user 0m0.556s +sys 0m0.113s + + + ============================================================================== + +Running: pyright --stats ./src + +Found 2 source files +pyright 1.1.359 +0 errors, 0 warnings, 0 informations +Completed in 0.729sec + +Analysis stats +Total files parsed and bound: 23 +Total files checked: 2 + +Timing stats +Find Source Files: 0sec +Read Source Files: 0.01sec +Tokenize: 0.05sec +Parse: 0.07sec +Resolve Imports: 0.06sec +Bind: 0.08sec +Check: 0.13sec +Detect Cycles: 0sec + +real 0m1.417s +user 0m1.709s +sys 0m0.198s + + + ============================================================================== + +Exit code: 0 + +real 0m3.213s +user 0m2.902s +sys 0m0.608s + +real 0m3.216s +user 0m2.903s +sys 0m0.610s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-bandit + +Running Python Lint - Bandit + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: bandit --version + +bandit 1.7.7 + python version = 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110] + +real 0m0.284s +user 0m0.211s +sys 0m0.076s + + + ============================================================================== + +Running: bandit --verbose --recursive ./src + +[main] INFO profile include tests: None +[main] INFO profile exclude tests: None +[main] INFO cli include tests: None +[main] INFO cli exclude tests: None +[main] INFO running on Python 3.12.1 +Run started:2024-04-18 04:23:49.345884 +Files in scope (2): + ./src/luhn/__init__.py (score: {SEVERITY: 0, CONFIDENCE: 0}) + ./src/luhn/luhn.py (score: {SEVERITY: 0, CONFIDENCE: 0}) +Files excluded (2): + ./src/luhn/__pycache__/__init__.cpython-312.pyc + ./src/luhn/__pycache__/luhn.cpython-312.pyc + +Test results: + No issues identified. + +Code scanned: + Total lines of code: 153 + Total lines skipped (#nosec): 0 + Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0 + +Run metrics: + Total issues (by severity): + Undefined: 0 + Low: 0 + Medium: 0 + High: 0 + Total issues (by confidence): + Undefined: 0 + Low: 0 + Medium: 0 + High: 0 +Files skipped (0): + +real 0m0.248s +user 0m0.173s +sys 0m0.078s + + + ============================================================================== + +Exit code: 0 + +real 0m1.449s +user 0m1.042s +sys 0m0.430s + +real 0m1.451s +user 0m1.044s +sys 0m0.430s + +=============================================================================== + +Running: ../../.github/citools/python/python-lint-refurb + +Running Python Lint - Refurb + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: refurb --version + +Refurb: v1.26.0 +Mypy: v1.9.0 + +real 0m0.195s +user 0m0.125s +sys 0m0.073s + + + ============================================================================== + +Running: refurb --quiet --ignore 183 ./src + + +real 0m1.265s +user 0m1.164s +sys 0m0.104s + + + ============================================================================== + +Exit code: 0 + +real 0m3.620s +user 0m3.114s +sys 0m0.532s + +real 0m3.623s +user 0m3.115s +sys 0m0.533s + +=============================================================================== + +Running: ../../.github/citools/python/python-test-with-doctests + +Running Python DocTests + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +PYTHONPATH="./src" + + ============================================================================== + +Running: python -m doctest -v ./src/luhn/__init__.py ./src/luhn/luhn.py + +1 items had no tests: + __init__ +0 tests in 1 items. +0 passed and 0 failed. +Test passed. +Trying: + l: Luhn = Luhn("055 444 285") +Expecting nothing +ok +Trying: + l._Luhn__is_luhn_number() +Expecting: + True +ok +Trying: + l: Luhn = Luhn("055 444 286") +Expecting nothing +ok +Trying: + l._Luhn__is_luhn_number() +Expecting: + False +ok +Trying: + digits: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + digits +Expecting: + [0, 5, 5, 4, 4, 4, 2, 8, 6] +ok +Trying: + numbers: list[int] = l._Luhn__step_one_and_two(digits) +Expecting nothing +ok +Trying: + numbers +Expecting: + [1, 5, 8, 4, 8, 2, 7, 6, 0] +ok +Trying: + digit_sum: int = sum(numbers) +Expecting nothing +ok +Trying: + digit_sum +Expecting: + 41 +ok +Trying: + l: Luhn = Luhn("234 567 891 234") +Expecting nothing +ok +Trying: + l._Luhn__is_luhn_number() +Expecting: + True +ok +Trying: + digits: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + digits +Expecting: + [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] +ok +Trying: + numbers: list[int] = l._Luhn__step_one_and_two(digits) +Expecting nothing +ok +Trying: + numbers +Expecting: + [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] +ok +Trying: + digit_sum: int = sum(numbers) +Expecting nothing +ok +Trying: + digit_sum +Expecting: + 60 +ok +Trying: + l: Luhn = Luhn("0123456789") +Expecting nothing +ok +Trying: + d: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + d +Expecting: + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +ok +Trying: + l._Luhn__step_one_and_two(d) +Expecting: + [0, 1, 4, 3, 8, 5, 3, 7, 7, 9] +ok +Trying: + l: Luhn = Luhn("59") +Expecting nothing +ok +Trying: + d: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + d +Expecting: + [5, 9] +ok +Trying: + s = l._Luhn__step_one_and_two(d) +Expecting nothing +ok +Trying: + s +Expecting: + [1, 9] +ok +Trying: + sum(s) +Expecting: + 10 +ok +Trying: + l: Luhn = Luhn(" 0") +Expecting nothing +ok +Trying: + d: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + d +Expecting: + [0] +ok +Trying: + l._Luhn__step_one_and_two(d) +Expecting: + [0] +ok +Trying: + sum(l._Luhn__step_one_and_two(d)) +Expecting: + 0 +ok +Trying: + l: Luhn = Luhn("234 567 891 234") +Expecting nothing +ok +Trying: + d: list[int] = l.code.extract_digits() +Expecting nothing +ok +Trying: + d +Expecting: + [2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4] +ok +Trying: + l._Luhn__step_one_and_two(d) +Expecting: + [4, 3, 8, 5, 3, 7, 7, 9, 2, 2, 6, 4] +ok +Trying: + sum(l._Luhn__step_one_and_two(d)) +Expecting: + 57 +ok +Trying: + l: Luhn = Luhn("055 444 285") +Expecting nothing +ok +Trying: + l.valid() +Expecting: + True +ok +Trying: + l: Luhn = Luhn("055 444 286") +Expecting nothing +ok +Trying: + l.valid() +Expecting: + False +ok +Trying: + l: Luhn = Luhn("234 567 891 234") +Expecting nothing +ok +Trying: + l.valid() +Expecting: + True +ok +Trying: + n: String = String("234 567 891 234") +Expecting nothing +ok +Trying: + n == "0" +Expecting: + False +ok +Trying: + len(n) == 0 +Expecting: + False +ok +Trying: + n == "0" +Expecting: + False +ok +Trying: + n.match(r"^([0-9 ])+$") +Expecting: + True +ok +Trying: + s: String = String("1234 5678") +Expecting nothing +ok +Trying: + s.extract_digits() +Expecting: + [1, 2, 3, 4, 5, 6, 7, 8] +ok +Trying: + s: String = String("1234 5678") +Expecting nothing +ok +Trying: + s +Expecting: + '1234 5678' +ok +Trying: + s.match(r"^[0-9 ]+$") +Expecting: + True +ok +Trying: + s.match(r"^[a-z ]+$") +Expecting: + False +ok +4 items had no tests: + luhn + luhn.Luhn + luhn.Luhn.__init__ + luhn.String +5 items passed all tests: + 18 tests in luhn.Luhn._Luhn__is_luhn_number + 20 tests in luhn.Luhn._Luhn__step_one_and_two + 11 tests in luhn.Luhn.valid + 2 tests in luhn.String.extract_digits + 4 tests in luhn.String.match +55 tests in 9 items. +55 passed and 0 failed. +Test passed. + +real 0m0.129s +user 0m0.083s +sys 0m0.048s + + + ============================================================================== + +Exit code: 0 + +real 0m1.067s +user 0m0.748s +sys 0m0.339s + +real 0m1.069s +user 0m0.750s +sys 0m0.339s + +=============================================================================== + +Running: ../../.github/citools/python/python-test-with-coverage + +Running Python Tests With Coverage + +Python versions: + + Python 3.12.1 + pyenv 2.3.36-21-g7e550e31 + pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12) + PDM, version 2.13.2 + + + ============================================================================== + +Running: rm -rf ./coverage + + +real 0m0.001s +user 0m0.000s +sys 0m0.001s + + + ============================================================================== + +Running: pytest --version + +pytest 7.4.3 + +real 0m0.819s +user 0m0.863s +sys 0m0.806s + + + ============================================================================== + +PYTHONPATH="./src" + + ============================================================================== + +Running: pytest --verbose --cov=. --cov-branch --cov-report=term-missing --cov-report=xml:.coverage.xml -p no:randomly ./test + +============================= test session starts ============================== +platform linux -- Python 3.12.1, pytest-7.4.3, pluggy-1.3.0 -- /home/vpayno/.pyenv/versions/3.12.1/bin/python +cachedir: .pytest_cache +hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/vpayno/git_vpayno/exercism-workspace/python/luhn/.hypothesis/examples')) +rootdir: /home/vpayno/git_vpayno/exercism-workspace/python/luhn +configfile: pytest.ini +plugins: anyio-4.2.0, libtmux-0.25.0, pylama-8.4.1, cov-4.1.0, datafiles-3.0.0, docker-2.0.1, subprocess-1.5.0, typeguard-4.1.5, hypothesis-6.98.17, snapshot-0.9.0 +collecting ... collected 23 items + +test/luhn_test.py::LuhnTest::test_a_simple_valid_sin_that_becomes_invalid_if_reversed PASSED [ 4%] +test/luhn_test.py::LuhnTest::test_a_simple_valid_sin_that_remains_valid_if_reversed PASSED [ 8%] +test/luhn_test.py::LuhnTest::test_a_single_zero_is_invalid PASSED [ 13%] +test/luhn_test.py::LuhnTest::test_a_valid_canadian_sin PASSED [ 17%] +test/luhn_test.py::LuhnTest::test_input_digit_9_is_correctly_converted_to_output_digit_9 PASSED [ 21%] +test/luhn_test.py::LuhnTest::test_invalid_canadian_sin PASSED [ 26%] +test/luhn_test.py::LuhnTest::test_invalid_credit_card PASSED [ 30%] +test/luhn_test.py::LuhnTest::test_invalid_long_number_with_a_remainder_divisible_by_5 PASSED [ 34%] +test/luhn_test.py::LuhnTest::test_invalid_long_number_with_an_even_remainder PASSED [ 39%] +test/luhn_test.py::LuhnTest::test_is_valid_can_be_called_repeatedly PASSED [ 43%] +test/luhn_test.py::LuhnTest::test_more_than_a_single_zero_is_valid PASSED [ 47%] +test/luhn_test.py::LuhnTest::test_non_numeric_non_space_char_in_the_middle_with_a_sum_that_s_divisible_by_10_isn_t_allowed PASSED [ 52%] +test/luhn_test.py::LuhnTest::test_single_digit_strings_can_not_be_valid PASSED [ 56%] +test/luhn_test.py::LuhnTest::test_single_zero_with_space_is_invalid PASSED [ 60%] +test/luhn_test.py::LuhnTest::test_using_ascii_value_for_doubled_non_digit_isn_t_allowed PASSED [ 65%] +test/luhn_test.py::LuhnTest::test_using_ascii_value_for_non_doubled_non_digit_isn_t_allowed PASSED [ 69%] +test/luhn_test.py::LuhnTest::test_valid_luhn_with_an_odd_number_of_digits_and_non_zero_first_digit PASSED [ 73%] +test/luhn_test.py::LuhnTest::test_valid_number_with_an_even_number_of_digits PASSED [ 78%] +test/luhn_test.py::LuhnTest::test_valid_number_with_an_odd_number_of_spaces PASSED [ 82%] +test/luhn_test.py::LuhnTest::test_valid_strings_with_a_non_digit_added_at_the_end_become_invalid PASSED [ 86%] +test/luhn_test.py::LuhnTest::test_valid_strings_with_punctuation_included_become_invalid PASSED [ 91%] +test/luhn_test.py::LuhnTest::test_valid_strings_with_symbols_included_become_invalid PASSED [ 95%] +test/luhn_test.py::LuhnTest::test_very_long_input_is_valid PASSED [100%] + +=============================== warnings summary =============================== +../../../../.pyenv/versions/3.12.1/lib/python3.12/site-packages/coverage/pytracer.py:164 + /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/coverage/pytracer.py:164: DeprecationWarning: currentThread() is deprecated, use current_thread() instead + self.thread = self.threading.currentThread() + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html + +---------- coverage: platform linux, python 3.12.1-final-0 ----------- +Name Stmts Miss Branch BrPart Cover Missing +-------------------------------------------------------------- +luhn.py 38 1 10 1 96% 52->exit +test/__init__.py 0 0 0 0 100% +-------------------------------------------------------------- +TOTAL 38 1 10 1 96% +Coverage XML written to file .coverage.xml + +======================== 23 passed, 1 warning in 0.99s ========================= + +real 0m1.895s +user 0m1.749s +sys 0m0.148s + + + ============================================================================== + +Running: coverage report --show-missing + +Name Stmts Miss Branch BrPart Cover Missing +-------------------------------------------------------------- +luhn.py 38 1 10 1 96% 52->exit +test/__init__.py 0 0 0 0 100% +-------------------------------------------------------------- +TOTAL 38 1 10 1 96% + +real 0m0.167s +user 0m0.091s +sys 0m0.079s + + + ============================================================================== + +Running: coverage annotate + + +real 0m0.149s +user 0m0.091s +sys 0m0.061s + + + ============================================================================== + +Line Coverage: 97.4% +Branch Coverage: 90.0% + + ============================================================================== + +Exit code: 0 + +real 0m3.971s +user 0m3.466s +sys 0m1.380s + +real 0m3.973s +user 0m3.466s +sys 0m1.382s + +=============================================================================== + +tail -n 10000 ./*,cover | grep -E -C 3 '^> def |^! ' + +=============================================================================== + +Running: misspell ./src/luhn/__init__.py ./src/luhn/luhn.py + +real 0m0.019s +user 0m0.017s +sys 0m0.011s + +=============================================================================== + diff --git a/python/luhn/src/luhn/__init__.py b/python/luhn/src/luhn/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/luhn/src/luhn/luhn.py b/python/luhn/src/luhn/luhn.py new file mode 120000 index 00000000..0afe5d25 --- /dev/null +++ b/python/luhn/src/luhn/luhn.py @@ -0,0 +1 @@ +../../luhn.py \ No newline at end of file diff --git a/python/luhn/test/__init__.py b/python/luhn/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/luhn/test/__init__.py,cover b/python/luhn/test/__init__.py,cover new file mode 100644 index 00000000..e69de29b diff --git a/python/luhn/test/luhn_test.py b/python/luhn/test/luhn_test.py new file mode 120000 index 00000000..578e7971 --- /dev/null +++ b/python/luhn/test/luhn_test.py @@ -0,0 +1 @@ +../luhn_test.py \ No newline at end of file