diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..4e975a7 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,28 @@ +name: Pylint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install -r requirements.txt + - name: Analysing the code with pylint + run: | + python -m pylint --verbose $(find . -name "*.py" ! -path "*/.venv/*" ! -path "*/venv/*") --rcfile=.pylintrc \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..c9c9d86 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,29 @@ +name: Pytest Workflow + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + + - name: Run pytest + run: pytest --verbose --ignore=solutions \ No newline at end of file diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..f493ef7 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,25 @@ +name: Ruff Lint and Format + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Ruff + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Ruff lint + run: ruff check --output-format=github . + - name: Run Ruff format check + run: ruff format --check . \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..97d344e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,3 @@ +[MASTER] +ignore=.venv +ignore-paths=^(.*/|)solutions/.*$ \ No newline at end of file diff --git a/README.md b/README.md index 6fd8298..078237e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,25 @@ -# Exercism Python Track +# [Exercism Python Track](https://exercism.io/my/tracks/python) -Learn and master concepts to achieve fluency in Python. +[![Pytest Workflow](https://github.com/ikostan/python/actions/workflows/pytest.yml/badge.svg)](https://github.com/ikostan/python/actions/workflows/pytest.yml) +[![Pylint](https://github.com/ikostan/python/actions/workflows/pylint.yml/badge.svg)](https://github.com/ikostan/python/actions/workflows/pylint.yml) +[![Ruff Lint and Format](https://github.com/ikostan/python/actions/workflows/ruff.yml/badge.svg)](https://github.com/ikostan/python/actions/workflows/ruff.yml) + +
+ +
+ +## Exercism exercises in Python + +### About Exercism +Exercism is an online platform designed to help you improve your coding skills through practice and mentorship. + +Exercism provides you with thousands of exercises spread across numerous language tracks. Once you start a language track you are presented with a core set of exercises to complete. Each one is a fun and interesting challenge designed to teach you a little more about the features of a language. + +You complete a challenge by downloading the exercise to your computer and solving it in your normal working environment. Once you've finished you submit it online and one of our mentors will give you feedback on how you could improve it using features of the language that you may not be familiar with. After a couple of rounds of refactoring, your exercise will be complete and you will unlock both the next core exercise and also a series of related side-exercises for you to practice with. + +Exercism is entirely open source and relies on the contributions of thousands of wonderful people. + +Exercism is designed to be fun and friendly, and we place a strong emphasis on empathetic communication. + +Sign up and have fun. Exercism is 100% free :) -143 coding exercises for Python on Exercism. From Error Handling to ISBN Verifier. diff --git a/armstrong-numbers/armstrong_numbers_test.py b/armstrong-numbers/armstrong_numbers_test.py index 4024766..092f75d 100644 --- a/armstrong-numbers/armstrong_numbers_test.py +++ b/armstrong-numbers/armstrong_numbers_test.py @@ -1,3 +1,46 @@ +# pylint: disable=C0301 +""" +Armstrong Numbers Test Suite Documentation + +## Overview + +This test suite validates the `is_armstrong_number` function, +ensuring its correct behavior for various types of numbers: + +- Single-digit and multi-digit numbers +- Known Armstrong numbers and non-Armstrong numbers + +Tests are auto-generated based on canonical data from +[Exercism problem specifications](https://github.com/exercism/problem-specifications/tree/main/exercises/armstrong-numbers/canonical-data.json). + +## Structure + +- **Framework:** Uses Python's built-in `unittest`. +- **Target Function:** `is_armstrong_number` (imported from `armstrong_numbers` module). + +## Test Cases + +| Test Description | Input | Expected Output | +|---------------------------------------------------------|----------|-----------------| +| Zero is an Armstrong number | 0 | `True` | +| Single-digit numbers are Armstrong numbers | 5 | `True` | +| No two-digit numbers (e.g. 10) are Armstrong numbers | 10 | `False` | +| 153 is an Armstrong number | 153 | `True` | +| 100 is not an Armstrong number | 100 | `False` | +| 9474 is an Armstrong number | 9474 | `True` | +| 9475 is not an Armstrong number | 9475 | `False` | +| 9926315 is an Armstrong number | 9926315 | `True` | +| 9926314 is not an Armstrong number | 9926314 | `False` | + +## Usage + +To run the tests, ensure `is_armstrong_number` is implemented and run: + +```bash +python -m unittest armstrong_numbers_test.py + +""" + # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/armstrong-numbers/canonical-data.json # File last updated on 2023-07-20 @@ -10,29 +53,88 @@ class ArmstrongNumbersTest(unittest.TestCase): + """Armstrong Numbers Test.""" + def test_zero_is_an_armstrong_number(self): + """ + Test that zero is correctly identified as an Armstrong number. + + This test verifies that the function correctly determines that 0 + is an Armstrong number, as 0^1 == 0. + + :returns: None + :rtype: NoneType + """ self.assertIs(is_armstrong_number(0), True) def test_single_digit_numbers_are_armstrong_numbers(self): + """ + Test that all single digit numbers are Armstrong numbers. + + :returns: None. Asserts that a single digit number (e.g., 5) is an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(5), True) def test_there_are_no_two_digit_armstrong_numbers(self): + """ + Test that no two-digit numbers are Armstrong numbers. + + :returns: None. Asserts that a two-digit number (e.g., 10) is not an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(10), False) def test_three_digit_number_that_is_an_armstrong_number(self): + """ + Test that 153 is correctly identified as an Armstrong number. + + :returns: None. Asserts that 153 is an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(153), True) def test_three_digit_number_that_is_not_an_armstrong_number(self): + """ + Test that 100 is not identified as an Armstrong number. + + :returns: None. Asserts that 100 is not an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(100), False) def test_four_digit_number_that_is_an_armstrong_number(self): + """ + Test that 9474 is correctly identified as an Armstrong number. + + :returns: None. Asserts that 9474 is an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(9474), True) def test_four_digit_number_that_is_not_an_armstrong_number(self): + """ + Test that 9475 is not identified as an Armstrong number. + + :returns: None. Asserts that 9475 is not an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(9475), False) def test_seven_digit_number_that_is_an_armstrong_number(self): + """ + Test that 9926315 is correctly identified as an Armstrong number. + + :returns: None. Asserts that 9926315 is an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(9926315), True) def test_seven_digit_number_that_is_not_an_armstrong_number(self): + """ + Test that 9926314 is not identified as an Armstrong number. + + :returns: None. Asserts that 9926314 is not an Armstrong number. + :rtype: NoneType + """ self.assertIs(is_armstrong_number(9926314), False) diff --git a/black-jack/black_jack.py b/black-jack/black_jack.py index c252b88..4d9a3fe 100644 --- a/black-jack/black_jack.py +++ b/black-jack/black_jack.py @@ -17,9 +17,10 @@ def value_of_card(card) -> int: 2. 'A' (ace card) = 1 3. '2' - '10' = numerical value. """ - if card in 'JKQ': + if card in "JKQ": return 10 - elif card == 'A': + + if card == "A": return 1 return int(card) @@ -29,8 +30,10 @@ def higher_card(card_one, card_two) -> str | tuple[str, str]: """ Determine which card has a higher value in the hand. - :param card_one, card_two: str - cards dealt in hand. See below for values. - :return: str or tuple - resulting Tuple contains both cards if they are of equal value. + :param card_one: str - cards dealt in hand. See below for values. + :param card_two: str - cards dealt in hand. See below for values. + :return: str or tuple - resulting Tuple contains both cards if + they are of equal value. 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 2. 'A' (ace card) = 1 @@ -38,7 +41,8 @@ def higher_card(card_one, card_two) -> str | tuple[str, str]: """ if value_of_card(card_one) == value_of_card(card_two): return card_one, card_two - elif value_of_card(card_one) > value_of_card(card_two): + + if value_of_card(card_one) > value_of_card(card_two): return card_one return card_two @@ -56,11 +60,14 @@ def value_of_ace(card_one, card_two) -> int: 3. '2' - '10' = numerical value. """ total: int = value_of_card(card_one) + value_of_card(card_two) - # Hint: if we already have an ace in hand, then the value for the upcoming ace would be 1. - if card_one == 'A' or card_two == 'A': + # Hint: if we already have an ace in hand, then the value for + # the upcoming ace would be 1. + if card_one == "A" or card_two == "A": return 1 - # The value of the hand with the ace needs to be as high as possible without going over 21. - elif 21 - total >= 11: + # The value of the hand with the ace needs to be as high as + # possible without going over 21. + + if 21 - total >= 11: return 11 return 1 @@ -70,7 +77,10 @@ def is_blackjack(card_one, card_two) -> bool: """ Determine if the hand is a 'natural' or 'blackjack'. - :param card_one, card_two: str - card dealt. See below for values. + :param card_one: card dealt. See below for values. + :type card_one: str + :param card_two: card dealt. See below for values. + :type card_two: str :return: bool - is the hand is a blackjack (two cards worth 21). 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 @@ -79,9 +89,10 @@ def is_blackjack(card_one, card_two) -> bool: """ # If a player is dealt an ace (A) and a ten-card (10, K, Q, or J) # as their first two cards, then the player has a score of 21. - if card_one == 'A' and card_two in ('J', 'Q', 'K', '10'): + if card_one == "A" and card_two in ("J", "Q", "K", "10"): return True - elif card_two == 'A' and card_one in ('J', 'Q', 'K', '10'): + + if card_two == "A" and card_one in ("J", "Q", "K", "10"): return True return False @@ -91,8 +102,10 @@ def can_split_pairs(card_one, card_two) -> bool: """ Determine if a player can split their hand into two hands. - :param card_one, card_two: str - cards dealt. - :return: bool - can the hand be split into two pairs? (i.e. cards are of the same value). + :param card_one: str - cards dealt. + :param card_two: str - cards dealt. + :return: bool - can the hand be split into two pairs? + (i.e. cards are of the same value). """ if value_of_card(card_one) == value_of_card(card_two): return True @@ -104,7 +117,9 @@ def can_double_down(card_one, card_two) -> bool: """ Determine if a blackjack player can place a double down bet. - :param card_one, card_two: str - first and second cards in hand. - :return: bool - can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). + :param card_one: str - first and second cards in hand. + :param card_two: str - first and second cards in hand. + :return: bool - can the hand can be doubled down? + (i.e. totals 9, 10 or 11 points). """ return 9 <= value_of_card(card_one) + value_of_card(card_two) <= 11 diff --git a/black-jack/black_jack_test.py b/black-jack/black_jack_test.py index 0962781..f01f2df 100644 --- a/black-jack/black_jack_test.py +++ b/black-jack/black_jack_test.py @@ -1,114 +1,188 @@ +# pylint: disable=C0301 +""" +Unit tests suite for blackjack game logic functions. + +This test case class covers the following functionalities: + + - :func:`value_of_card`: Evaluates the value of individual blackjack cards. + - :func:`higher_card`: Determines the higher value card or equality. + - :func:`value_of_ace`: Calculates optimal ace value given two cards. + - :func:`is_blackjack`: Checks if a hand is a 'blackjack' (natural 21). + - :func:`can_split_pairs`: Determines if a hand can be split into two pairs. + - :func:`can_double_down`: Assesses eligibility to double down. + +Tests use parameterized cases with informative error messages for each assertion. +""" + import unittest +from parameterized import parameterized import pytest from black_jack import ( - value_of_card, - higher_card, - value_of_ace, - is_blackjack, - can_split_pairs, - can_double_down - ) + value_of_card, + higher_card, + value_of_ace, + is_blackjack, + can_split_pairs, + can_double_down +) +# pylint: disable=C0301 class BlackJackTest(unittest.TestCase): + """ + Unit test suite for blackjack game logic functions. - @pytest.mark.task(taskno=1) - def test_value_of_card(self): - test_data = [('2', 2), ('5', 5), ('8', 8), - ('A', 1), ('10', 10), ('J', 10), - ('Q', 10), ('K', 10)] - - for variant, (card, expected) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', card=card, expected=expected): - actual_result = value_of_card(card) - error_msg = (f'Called value_of_card({card}). ' - f'The function returned {actual_result} as the value of the {card} card, ' - f'but the test expected {expected} as the {card} card value.') + Each test validates a specific utility function from the blackjack module: + - :func:`test_value_of_card`: Checks the correct card value assignment. + - :func:`test_higher_card`: Ensures the function returns the card with higher value or both if equal. + - :func:`test_value_of_ace`: Verifies optimal ace value calculation based on the current hand. + - :func:`test_is_blackjack`: Determines if a two-card hand is a blackjack. + - :func:`test_can_split_pairs`: Tests if two cards can be split into two separate hands. + - :func:`test_can_double_down`: Evaluates if the hand is eligible for doubling down. - self.assertEqual(actual_result, expected, msg=error_msg) + Tests are parameterized for thorough coverage and use descriptive assertion error messages. + """ + @pytest.mark.task(taskno=1) + @parameterized.expand([ + ('2', 2), ('5', 5), ('8', 8), + ('A', 1), ('10', 10), ('J', 10), + ('Q', 10), ('K', 10), + ]) + def test_value_of_card(self, card, expected): + """ + Test that the value_of_card function returns the correct + value for a given card. + + :param card: Card to evaluate. + :type card: str + :param expected: Expected value for the provided card. + :type expected: int + """ + actual_result = value_of_card(card) + error_msg = (f'Called value_of_card({card}). ' + f'The function returned {actual_result} ' + f'as the value of ' + f'the {card} card, ' + f'but the test expected {expected} as the ' + f'{card} card value.') + self.assertEqual(actual_result, expected, msg=error_msg) @pytest.mark.task(taskno=2) - def test_higher_card(self): - test_data = [('A', 'A', ('A', 'A')), - ('10', 'J', ('10', 'J')), - ('3', 'A', '3'), - ('3', '6', '6'), - ('Q', '10', ('Q', '10')), - ('4', '4', ('4', '4')), - ('9', '10', '10'), - ('6', '9', '9'), - ('4', '8', '8')] - - for variant, (card_one, card_two, expected) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', card_one=card_one, card_two=card_two, expected=expected): - actual_result = higher_card(card_one, card_two) - error_msg = (f'Called higher_card({card_one}, {card_two}). ' - f'The function returned {actual_result}, ' - f'but the test expected {expected} as the result for the cards {card_one, card_two}.') - - self.assertEqual(actual_result, expected, msg=error_msg) + @parameterized.expand([ + ('A', 'A', ('A', 'A')), + ('10', 'J', ('10', 'J')), + ('3', 'A', '3'), + ('3', '6', '6'), + ('Q', '10', ('Q', '10')), + ('4', '4', ('4', '4')), + ('9', '10', '10'), + ('6', '9', '9'), + ('4', '8', '8'), + ]) + def test_higher_card(self, card_one, card_two, expected): + """ + Test that the higher_card function correctly determines + which card has the higher value. + + :param card_one: First card to compare. + :param card_two: Second card to compare. + :param expected: The expected result for the higher card. + """ + actual_result = higher_card(card_one, card_two) + error_msg = (f'Called higher_card({card_one}, {card_two}). ' + f'The function returned {actual_result}, ' + f'but the test expected {expected} as the result for ' + f'the cards {card_one, card_two}.') + self.assertEqual(actual_result, expected, msg=error_msg) @pytest.mark.task(taskno=3) - def test_value_of_ace(self): - test_data = [('2', '3', 11), ('3', '6', 11), ('5', '2', 11), - ('8', '2', 11), ('5', '5', 11), ('Q', 'A', 1), - ('10', '2', 1), ('7', '8', 1), ('J', '9', 1), - ('K', 'K', 1), ('2', 'A', 1), ('A', '2', 1)] - - for variant, (card_one, card_two, ace_value) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', card_one=card_one, card_two=card_two, ace_value=ace_value): - actual_result = value_of_ace(card_one, card_two) - error_msg = (f'Called value_of_ace({card_one}, {card_two}). ' - f'The function returned {actual_result}, ' - f'but the test expected {ace_value} as the value of an ace card ' - f'when the hand includes {card_one, card_two}.') - - self.assertEqual(value_of_ace(card_one, card_two), ace_value, msg=error_msg) + @parameterized.expand([ + ('2', '3', 11), ('3', '6', 11), ('5', '2', 11), + ('8', '2', 11), ('5', '5', 11), ('Q', 'A', 1), + ('10', '2', 1), ('7', '8', 1), ('J', '9', 1), + ('K', 'K', 1), ('2', 'A', 1), ('A', '2', 1), + ]) + def test_value_of_ace(self, card_one, card_two, ace_value): + """ + Test that the value_of_ace function returns the correct ace value + given the other two cards. + + :param card_one: The first card in hand. + :param card_two: The second card in hand. + :param ace_value: The expected ace value. + """ + actual_result = value_of_ace(card_one, card_two) + error_msg = (f'Called value_of_ace({card_one}, {card_two}). ' + f'The function returned {actual_result}, ' + f'but the test expected {ace_value} ' + f'as the value of an ace card ' + f'when the hand includes {card_one, card_two}.') + self.assertEqual(actual_result, ace_value, msg=error_msg) @pytest.mark.task(taskno=4) - def test_is_blackjack(self): - test_data = [(('A', 'K'), True), (('10', 'A'), True), - (('10', '9'), False), (('A', 'A'), False), - (('4', '7'), False), (('9', '2'), False), - (('Q', 'K'), False)] - - for variant, (hand, expected) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', hand=hand, expected=expected): - actual_result = is_blackjack(*hand) - error_msg = (f'Called is_blackjack({hand[0]}, {hand[1]}). ' - f'The function returned {actual_result}, ' - f'but hand {hand} {"is" if expected else "is not"} a blackjack.') - - self.assertEqual(actual_result, expected, msg=error_msg) + @parameterized.expand([ + (('A', 'K'), True), (('10', 'A'), True), + (('10', '9'), False), (('A', 'A'), False), + (('4', '7'), False), (('9', '2'), False), + (('Q', 'K'), False), + ]) + def test_is_blackjack(self, hand, expected): + """ + Test if a given hand qualifies as blackjack. + + :param hand: List representing the player's hand. + :param expected: Expected boolean indicating if the hand is a blackjack. + """ + actual_result = is_blackjack(*hand) + error_msg = (f'Called is_blackjack({hand[0]}, {hand[1]}). ' + f'The function returned {actual_result}, ' + f'but hand {hand} {"is" if expected else "is not"} ' + f'a blackjack.') + self.assertEqual(actual_result, expected, msg=error_msg) @pytest.mark.task(taskno=5) - def test_can_split_pairs(self): - test_data = [(('Q', 'K'), True), (('6', '6'), True), - (('A', 'A'), True),(('10', 'A'), False), - (('10', '9'), False)] - - for variant, (hand, expected) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', input=hand, expected=expected): - actual_result = can_split_pairs(*hand) - error_msg = (f'Called can_split_pairs({hand[0]}, {hand[1]}). ' - f'The function returned {actual_result}, ' - f'but hand {hand} {"can" if expected else "cannot"} be split into pairs.') - - self.assertEqual(actual_result, expected, msg=error_msg) + @parameterized.expand([ + (('Q', 'K'), True), (('6', '6'), True), + (('A', 'A'), True), (('10', 'A'), False), + (('10', '9'), False), + ]) + def test_can_split_pairs(self, hand, expected): + """ + Test whether the `can_split_pairs` function correctly + determines if a given blackjack hand can be split into pairs. + + :param hand: List representing the player's hand. + :param expected: Expected boolean result indicating if split is allowed. + """ + actual_result = can_split_pairs(*hand) + error_msg = (f'Called can_split_pairs({hand[0]}, {hand[1]}). ' + f'The function returned {actual_result}, ' + f'but hand {hand} ' + f'{"can" if expected else "cannot"} ' + f'be split into pairs.') + self.assertEqual(actual_result, expected, msg=error_msg) @pytest.mark.task(taskno=6) - def test_can_double_down(self): - test_data = [(('A', '9'), True), (('K', 'A'), True), - (('4', '5'), True),(('A', 'A'), False), - (('10', '2'), False), (('10', '9'), False)] - - for variant, (hand, expected) in enumerate(test_data, 1): - with self.subTest(f'variation #{variant}', hand=hand, expected=expected): - actual_result = can_double_down(*hand) - error_msg = (f'Called can_double_down({hand[0]}, {hand[1]}). ' - f'The function returned {actual_result}, ' - f'but hand {hand} {"can" if expected else "cannot"} be doubled down.') - - self.assertEqual(actual_result, expected, msg=error_msg) + @parameterized.expand([ + (('A', '9'), True), (('K', 'A'), True), + (('4', '5'), True), (('A', 'A'), False), + (('10', '2'), False), (('10', '9'), False), + ]) + def test_can_double_down(self, hand, expected): + """ + Test whether a hand qualifies to double down in blackjack. + + :param hand: The current cards in the player's hand. + :type hand: list + :param expected: The expected boolean result of whether the + hand can double down. + :type expected: bool + """ + actual_result = can_double_down(*hand) + error_msg = (f'Called can_double_down({hand[0]}, {hand[1]}). ' + f'The function returned {actual_result}, ' + f'but hand {hand} {"can" if expected else "cannot"} ' + f'be doubled down.') + self.assertEqual(actual_result, expected, msg=error_msg) diff --git a/card-games/.exercism/config.json b/card-games/.exercism/config.json new file mode 100644 index 0000000..f7d39ca --- /dev/null +++ b/card-games/.exercism/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "itamargal", + "isaacg", + "bethanyg" + ], + "contributors": [ + "valentin-p", + "pranasziaukas" + ], + "files": { + "solution": [ + "lists.py" + ], + "test": [ + "lists_test.py" + ], + "exemplar": [ + ".meta/exemplar.py" + ] + }, + "icon": "poker", + "blurb": "Learn about lists by tracking hands in card games." +} diff --git a/card-games/.exercism/metadata.json b/card-games/.exercism/metadata.json new file mode 100644 index 0000000..a6a97ce --- /dev/null +++ b/card-games/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"card-games","id":"d469c70514b141cba1dca4d2b26615c1","url":"https://exercism.org/tracks/python/exercises/card-games","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/card-games/HELP.md b/card-games/HELP.md new file mode 100644 index 0000000..7ff7fcc --- /dev/null +++ b/card-games/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 lists.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/card-games/HINTS.md b/card-games/HINTS.md new file mode 100644 index 0000000..3c10eca --- /dev/null +++ b/card-games/HINTS.md @@ -0,0 +1,47 @@ +# Hints + +## General + +## 1. Tracking Poker Rounds + +- Lists in Python may be [constructed][constructed] in multiple ways. +- This function should [return][return] a `list`. + +## 2. Keeping all Rounds in the Same Place + +- Sequence types such as `list` support [common operations][common sequence operations]. +- This function should [return][return] a `list`. + +## 3. Finding Prior Rounds + +- Sequence types such as `list` support a few [common operations][common sequence operations]. +- This function should [return][return] a `bool`. + +## 4. Averaging Card Values + +- To get the average, this function should count how many items are in the `list` and sum up their values. Then, return the sum divided by the count. + +## 5. Alternate Averages + +- Sequence types such as `list` support a few [common operations][common sequence operations]. +- To access an element, use the square brackets (`[]`) notation. +- Remember that the first element of the `list` is at index 0 from the **left-hand** side. +- In Python, negative indexing starts at -1 from the **right-hand** side. This means that you can find the last element of a `list` by using `[-1]`. +- Think about how you could reuse the code from the functions that you have already implemented. + +## 6. More Averaging Techniques + +- Sequence types such as `list` already support a few [common operations][common sequence operations]. +- Think about reusing the code from the functions that you just implemented. +- The slice syntax supports a _step value_ (`[::]`). + +## 7. Bonus Round Rules + +- Lists are _mutable_. Once a `list` is created, you can modify, delete or add any type of element you wish. +- Python provides a wide range of [ways to modify `lists`][ways to modify `lists`]. + + +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range +[constructed]: https://docs.python.org/3/library/stdtypes.html#list +[return]: https://www.w3schools.com/python/ref_keyword_return.asp +[ways to modify `lists`]: https://realpython.com/python-lists-tuples/#lists-are-mutable \ No newline at end of file diff --git a/card-games/README.md b/card-games/README.md new file mode 100644 index 0000000..a8334e3 --- /dev/null +++ b/card-games/README.md @@ -0,0 +1,393 @@ +# Card Games + +Welcome to Card Games on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +A [`list`][list] is a mutable collection of items in _sequence_. +Like most collections (_see the built-ins [`tuple`][tuple], [`dict`][dict] and [`set`][set]_), lists can hold reference to any (or multiple) data type(s) - including other lists. +Like any [sequence][sequence type], items can be accessed via `0-based index` number from the left and `-1-based index` from the right. +Lists can be copied in whole or in part via [slice notation][slice notation] or `.copy()`. + +Lists support both [common][common sequence operations] and [mutable][mutable sequence operations] sequence operations such as `min()`/`max()`, `.index()`, `.append()` and `.reverse()`. +List elements can be iterated over using the `for item in ` construct. + `for index, item in enumerate()` can be used when both the element index and the element value are needed. + +Under the hood, `lists` are implemented as [dynamic arrays][dynamic array] -- similar to Java's [`ArrayList`][arraylist] type, and are most often used to store groups of similar data (_strings, numbers, sets etc._) of unknown length. +Lists are an extremely flexible and useful data structure and many built-in methods and operations in Python produce lists as their output. + + +## Construction + +A `list` can be declared as a _literal_ with square `[]` brackets and commas between elements: + + +```python +>>> no_elements = [] + +>>> no_elements +[] + +>>> one_element = ["Guava"] + +>>> one_element +['Guava'] + +>>> elements_separated_with_commas = ["Parrot", "Bird", 334782] + +>>> elements_separated_with_commas +['Parrot', 'Bird', 334782] +``` + +For readability, line breaks can be used when there are many elements or nested data structures within a `list`: + + +```python +>>> lots_of_entries = [ + "Rose", + "Sunflower", + "Poppy", + "Pansy", + "Tulip", + "Fuchsia", + "Cyclamen", + "Lavender" + ] + +>>> lots_of_entries +['Rose', 'Sunflower', 'Poppy', 'Pansy', 'Tulip', 'Fuchsia', 'Cyclamen', 'Lavender'] + +# Each data structure is on its own line to help clarify what they are. +>>> nested_data_structures = [ + {"fish": "gold", "monkey": "brown", "parrot": "grey"}, + ("fish", "mammal", "bird"), + ['water', 'jungle', 'sky'] + ] + +>>> nested_data_structures +[{'fish': 'gold', 'monkey': 'brown', 'parrot': 'grey'}, ('fish', 'mammal', 'bird'), ['water', 'jungle', 'sky']] +``` + +The `list()` constructor can be used empty or with an _iterable_ as an argument. + Elements in the iterable are cycled through by the constructor and added to the `list` in order: + + +```python +>>> no_elements = list() + +>>> no_elements +[] + +# The tuple is unpacked and each element is added. +>>> multiple_elements_from_tuple = list(("Parrot", "Bird", 334782)) + +>>> multiple_elements_from_tuple +['Parrot', 'Bird', 334782] + +# The set is unpacked and each element is added. +>>> multiple_elements_from_set = list({2, 3, 5, 7, 11}) + +>>> multiple_elements_from_set +[2, 3, 5, 7, 11] +``` + +Results when using a `list` constructor with a `string` or a `dict` may be surprising: + + +```python +# String elements (Unicode code points) are iterated through and added *individually*. +>>> multiple_elements_string = list("Timbuktu") + +>>> multiple_elements_string +['T', 'i', 'm', 'b', 'u', 'k', 't', 'u'] + +# Unicode separators and positioning code points are also added *individually*. +>>> multiple_code_points_string = list('अभ्यास') + +>>> multiple_code_points_string +['अ', 'भ', '्', 'य', 'ा', 'स'] + +# The iteration default for dictionaries is over the keys, so only key data is inserted into the list. +>>> source_data = {"fish": "gold", "monkey": "brown"} + +>>> multiple_elements_dict_1 = list(source_data) +['fish', 'monkey'] +``` + +Because the `list` constructor will only take _iterables_ (or nothing) as arguments, objects that are _not_ iterable will throw a type error. + Consequently, it is much easier to create a one-item `list` via the literal method. + +```python +# Numbers are not iterable, and so attempting to create a list with a number passed to the constructor fails. +>>> one_element = list(16) +Traceback (most recent call last): + File "", line 1, in +TypeError: 'int' object is not iterable + +# Tuples *are* iterable, so passing a one-element tuple to the constructor does work, but it's awkward +>>> one_element_from_iterable = list((16,)) + +>>> one_element_from_iterable +[16] +``` + +## Accessing elements + +Items inside lists (_as well as items in other sequence types `str` & `tuple`_) can be accessed via `0-based index` and _bracket notation_. + Indexes can be from **`left`** --> **`right`** (_starting at zero_) or **`right`** --> **`left`** (_starting at -1_). + + + + + + +
index from left ⟹






+ +| 0
👇🏾 | 1
👇🏾 | 2
👇🏾 | 3
👇🏾 | 4
👇🏾 | 5
👇🏾 | +|:--------: |:--------: |:--------: |:--------: |:--------: |:--------: | +| P | y | t | h | o | n | +| 👆🏾
-6 | 👆🏾
-5 | 👆🏾
-4 | 👆🏾
-3 | 👆🏾
-2 | 👆🏾
-1 | +





⟸ index from right
+ + +```python +>>> breakfast_foods = ["Oatmeal", "Fruit Salad", "Eggs", "Toast"] + +# Oatmeal is at index 0 or index -4. +>>> breakfast_foods[0] +'Oatmeal' + +>>> breakfast_foods[-4] +'Oatmeal' + +# Eggs are at index -2 or 2 +>>> breakfast_foods[-2] +'Eggs' + +>>> breakfast_foods[2] +'Eggs' + +# Toast is at -1 +>>> breakfast_foods[-1] +'Toast' +``` + +A section of the elements inside a `list` can be accessed via _slice notation_ (`[start:stop]`). + A _slice_ is defined as an element sequence at position `index`, such that `start <= index < stop`. + _Slicing_ returns a copy of the "sliced" items and does not modify the original `list`. + + +A `step` parameter can also be used `[start:stop:step]` to "skip over" or filter the `list` elements (_for example, a `step` of 2 will select every other element in the range_): + + +```python +>>> colors = ["Red", "Purple", "Green", "Yellow", "Orange", "Pink", "Blue", "Grey"] + +# If there is no step parameter, the step is assumed to be 1. +>>> middle_colors = colors[2:6] + +>>> middle_colors +['Green', 'Yellow', 'Orange', 'Pink'] + +# If the start or stop parameters are omitted, the slice will +# start at index zero, and will stop at the end of the list. +>>> primary_colors = colors[::3] + +>>> primary_colors +['Red', 'Yellow', 'Blue'] +``` + +## Working with lists + +The usage of the built-in `sum()` function on a list will return the sum of all the numbers in the list: + +```python +>>> number_list = [1, 2, 3, 4] +>>> sum(number_list) +10 +``` + +You can also get the _length_ of a list by using the `len()` function: + +```python +>>> long_list = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] +>>> len(long_list) +10 +``` + +Lists can be also combined in various ways: + +```python +# Using the plus + operator unpacks each list and creates a new list, but it is not efficient. +>>> new_via_concatenate = ["George", 5] + ["cat", "Tabby"] + +>>> new_via_concatenate +['George', 5, 'cat', 'Tabby'] + +# Likewise, using the multiplication operator * is the equivalent of using + n times. +>>> first_group = ["cat", "dog", "elephant"] +>>> multiplied_group = first_group * 3 + +>>> multiplied_group +['cat', 'dog', 'elephant', 'cat', 'dog', 'elephant', 'cat', 'dog', 'elephant'] +``` + +Lists supply an _iterator_, and can be looped through/over in the same manner as other _sequence types_. + +```python +# Looping through the list and printing out each element. +>>> colors = ["Orange", "Green", "Grey", "Blue"] + +>>> for item in colors: +... print(item) +... +Orange +Green +Grey +Blue +``` + +_For a more in-depth explanation, of `loops` and `iterators`, complete the `loops` concept._ + +[arraylist]: https://beginnersbook.com/2013/12/java-arraylist/ +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[dict]: https://docs.python.org/3/library/stdtypes.html#dict +[dynamic array]: https://en.wikipedia.org/wiki/Dynamic_array +[list]: https://docs.python.org/3/library/stdtypes.html#list +[mutable sequence operations]: https://docs.python.org/3/library/stdtypes.html#typesseq-mutable +[sequence type]: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range +[set]: https://docs.python.org/3/library/stdtypes.html#set +[slice notation]: https://docs.python.org/3/reference/expressions.html#slicings +[tuple]: https://docs.python.org/3/library/stdtypes.html#tuple + +## Instructions + +Elyse is really looking forward to playing some poker (and other card games) during her upcoming trip to Vegas. + Being a big fan of "self-tracking" she wants to put together some small functions that will help her with tracking tasks and has asked for your help thinking them through. + +## 1. Tracking Poker Rounds + +Elyse is especially fond of poker, and wants to track how many rounds she plays - and _which rounds_ those are. + Every round has its own number, and every table shows the round number currently being played. + Elyse chooses a table and sits down to play her first round. She plans on playing three rounds. + +Implement a function `get_rounds()` that takes the current round number and returns a single `list` with that round and the _next two_ that are coming up: + +```python +>>> get_rounds(27) +[27, 28, 29] +``` + +## 2. Keeping all Rounds in the Same Place + +Elyse played a few rounds at the first table, then took a break and played some more rounds at a second table ... but ended up with a different list for each table! + She wants to put the two lists together, so she can track all of the poker rounds in the same place. + +Implement a function `concatenate_rounds(, )` that takes two lists and returns a single `list` consisting of all the rounds in the first `list`, followed by all the rounds in the second `list`: + +```python +>>> concatenate_rounds([27, 28, 29], [35, 36]) +[27, 28, 29, 35, 36] +``` + +## 3. Finding Prior Rounds + +Talking about some of the prior Poker rounds, another player remarks how similarly two of them played out. + Elyse is not sure if she played those rounds or not. + +Implement a function `list_contains_round(, )` that takes two arguments, a list of rounds played and a round number. + The function will return `True` if the round is in the list of rounds played, `False` if not: + +```python +>>> list_contains_round([27, 28, 29, 35, 36], 29) +True + +>>> list_contains_round([27, 28, 29, 35, 36], 30) +False +``` + +## 4. Averaging Card Values + +Elyse wants to try out a new game called Black Joe. + It's similar to Black Jack - where your goal is to have the cards in your hand add up to a target value - but in Black Joe the goal is to get the _average_ of the card values to be 7. + The average can be found by summing up all the card values and then dividing that sum by the number of cards in the hand. + +Implement a function `card_average()` that will return the average value of a hand of Black Joe. + +```python +>>> card_average([5, 6, 7]) +6.0 +``` + +## 5. Alternate Averages + +In Black Joe, speed is important. Elyse is going to try and find a faster way of finding the average. + +She has thought of two ways of getting an _average-like_ number: + +- Take the average of the _first_ and _last_ number in the hand. +- Using the median (middle card) of the hand. + +Implement the function `approx_average_is_average()`, given `hand`, a list containing the values of the cards in your hand. + +Return `True` if either _one_ `or` _both_ of the, above named, strategies result in a number _equal_ to the _actual average_. + +Note: _The length of all hands are odd, to make finding a median easier._ + +```python +>>> approx_average_is_average([1, 2, 3]) +True + +>>> approx_average_is_average([2, 3, 4, 8, 8]) +True + +>>> approx_average_is_average([1, 2, 3, 5, 9]) +False +``` + +## 6. More Averaging Techniques + +Intrigued by the results of her averaging experiment, Elyse is wondering if taking the average of the cards at the _even_ positions versus the average of the cards at the _odd_ positions would give the same results. + Time for another test function! + +Implement a function `average_even_is_average_odd()` that returns a Boolean indicating if the average of the cards at even indexes is the same as the average of the cards at odd indexes. + +```python +>>> average_even_is_average_odd([1, 2, 3]) +True + +>>> average_even_is_average_odd([1, 2, 3, 4]) +False +``` + +## 7. Bonus Round Rules + +Every 11th hand in Black Joe is a bonus hand with a bonus rule: if the last card you draw is a Jack, you double its value. + +Implement a function `maybe_double_last()` that takes a hand and checks if the last card is a Jack (11). + If the last card **is** a Jack (11), double its value before returning the hand. + +```python +>>> hand = [5, 9, 11] +>>> maybe_double_last(hand) +[5, 9, 22] + +>>> hand = [5, 9, 10] +>>> maybe_double_last(hand) +[5, 9, 10] +``` + +## Source + +### Created by + +- @itamargal +- @isaacg +- @bethanyg + +### Contributed to by + +- @valentin-p +- @pranasziaukas \ No newline at end of file diff --git a/card-games/lists.py b/card-games/lists.py new file mode 100644 index 0000000..ba00613 --- /dev/null +++ b/card-games/lists.py @@ -0,0 +1,80 @@ +""" +Functions for tracking poker hands and assorted card tasks. + +Python list documentation: https://docs.python.org/3/tutorial/datastructures.html +""" + + +def get_rounds(number: int) -> list[int]: + """ + Create a list containing the current and next two round numbers. + + :param number: int - current round number. + :return: list - current round and the two that follow. + """ + return [number, number + 1, number + 2] + + +def concatenate_rounds(rounds_1: list[int], rounds_2: list[int]) -> list[int]: + """ + Concatenate two lists of round numbers. + + :param rounds_1: list - first rounds played. + :param rounds_2: list - second set of rounds played. + :return: list - all rounds played. + """ + return rounds_1 + rounds_2 + + +def list_contains_round(rounds: list[int], number: int) -> bool: + """ + Check if the list of rounds contains the specified number. + + :param rounds: list - rounds played. + :param number: int - round number. + :return: bool - was the round played? + """ + return number in rounds + + +def card_average(hand: list[list]) -> float: + """ + Calculate and returns the average card value from the list. + + :param hand: list - cards in hand. + :return: float - average value of the cards in the hand. + """ + return sum(hand) / len(hand) + + +def approx_average_is_average(hand: list[int]) -> bool: + """ + Return if the (average of first and last card values) OR ('middle' card) == calculated average. + + :param hand: list - cards in hand. + :return: bool - does one of the approximate averages equal the `true average`? + """ + avg: float = card_average(hand) + return avg in ((hand[0] + hand[-1]) / 2, hand[len(hand) // 2]) + + +def average_even_is_average_odd(hand: list[int]) -> bool: + """ + Return if the (average of even indexed card values) == (average of odd indexed card values). + + :param hand: list - cards in hand. + :return: bool - are even and odd averages equal? + """ + even = hand[::2] + odd = hand[1::2] + return sum(even) / len(even) == sum(odd) / len(odd) + + +def maybe_double_last(hand: list[int]) -> list[int]: + """ + Multiply a Jack card value in the last index position by 2. + + :param hand: list - cards in hand. + :return: list - hand with Jacks (if present) value doubled. + """ + return hand if hand[-1] != 11 else hand[:-1] + [(hand[-1] * 2)] diff --git a/card-games/lists_test.py b/card-games/lists_test.py new file mode 100644 index 0000000..1c5386e --- /dev/null +++ b/card-games/lists_test.py @@ -0,0 +1,201 @@ +# pylint: disable=C0301 +""" +Unit tests for the card game utility functions in the lists module. + +Tests cover core behaviors such as generating rounds, concatenating rounds, +verifying round presence, calculating averages, comparing approximate and +actual averages, comparing even/odd index averages, and conditionally +doubling the last card value. + +Uses unittest framework with pytest marking for grading tasks. +""" +import unittest +import pytest + +from lists import ( + get_rounds, + concatenate_rounds, + list_contains_round, + card_average, + approx_average_is_average, + average_even_is_average_odd, + maybe_double_last, +) + + +class CardGamesTest(unittest.TestCase): + """Unit tests for card games utility functions.""" + + @pytest.mark.task(taskno=1) + def test_get_rounds(self): + """ + Test the get_rounds function with various inputs to ensure it returns + the current round and the next two rounds. + + :param self: The test case instance. + """ + input_data = [0, 1, 10, 27, 99, 666] + result_data = [[0, 1, 2], [1, 2, 3], + [10, 11, 12], [27, 28, 29], + [99, 100, 101], [666, 667, 668]] + + for variant, (number, expected) in enumerate( + zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + number=number, + expected=expected): + actual_result = get_rounds(number) + error_message = (f'Called get_rounds({number}). ' + f'The function returned {actual_result}, ' + f'but the tests expected rounds {expected} ' + f'given the current round {number}.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_concatenate_rounds(self): + """ + Test the concatenate_rounds function to ensure it correctly combines + two lists of rounds. + + :param self: The test case instance. + """ + input_data = [([], []), ([0, 1], []), ([], [1, 2]), + ([1], [2]), ([27, 28, 29], [35, 36]), + ([1, 2, 3], [4, 5, 6])] + + result_data = [[], [0, 1], [1, 2], [1, 2], + [27, 28, 29, 35, 36], + [1, 2, 3, 4, 5, 6]] + + for variant, ((rounds_1, rounds_2), expected) in enumerate( + zip(input_data, result_data), + start=1): + with self.subTest(f'variation #{variant}', + rounds_1=rounds_1, + rounds_2=rounds_2, + expected=expected): + actual_result = concatenate_rounds(rounds_1, rounds_2) + error_message = (f'Called concatenate_rounds({rounds_1}, {rounds_2}). ' + f'The function returned {actual_result}, but the tests ' + f'expected {expected} as the concatenation ' + f'of {rounds_1} and {rounds_2}.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_list_contains_round(self): + """ + Test the list_contains_round function to check if a specific round + is present in the list of played rounds. + + :param self: The test case instance. + """ + input_data = [([], 1), ([1, 2, 3], 0), + ([27, 28, 29, 35, 36], 30), + ([1], 1), ([1, 2, 3], 1), + ([27, 28, 29, 35, 36], 29)] + result_data = [False, False, False, True, True, True] + + for variant, ((rounds, round_number), expected) in enumerate( + zip(input_data, result_data), + start=1): + with self.subTest(f'variation #{variant}', + rounds=rounds, + round_number=round_number, + expected=expected): + actual_result = list_contains_round(rounds, round_number) + error_message = (f'Called list_contains_round({rounds}, {round_number}). ' + f'The function returned {actual_result}, but round {round_number} ' + f'{"is" if expected else "is not"} in {rounds}.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_card_average(self): + """ + Test the card_average function to ensure it calculates the correct + average of card values in a hand. + + :param self: The test case instance. + """ + input_data = [[1], [5, 6, 7], [1, 2, 3, 4], [1, 10, 100]] + result_data = [1.0, 6.0, 2.5, 37.0] + + for variant, (hand, expected) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', hand=hand, expected=expected): + actual_result = card_average(hand) + error_message = (f'Called card_average({hand}). ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} as the average of {hand}.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=5) + def test_approx_average_is_average(self): + """ + Test the approx_average_is_average function to check if approximate + averages match the actual average. + + :param self: The test case instance. + """ + input_data = [[0, 1, 5], [3, 6, 9, 12, 150], [1, 2, 3, 5, 9], + [2, 3, 4, 7, 8], [1, 2, 3], [2, 3, 4], + [2, 3, 4, 8, 8], [1, 2, 4, 5, 8]] + + result_data = [False, False, False, False, True, True, True, True] + + for variant, (hand, expected) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', hand=hand, expected=expected): + actual_result = approx_average_is_average(hand) + error_message = (f'Called approx_average_is_average({hand}). ' + f'The function returned {actual_result}, but ' + f'the hand {hand} {"does" if expected else "does not"} ' + f'yield the same approximate average.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=6) + def test_average_even_is_average_odd(self): + """ + Test the average_even_is_average_odd function to verify if averages + of even and odd indexed cards are equal. + + :param self: The test case instance. + """ + input_data = [[5, 6, 8], [1, 2, 3, 4], [1, 2, 3], [5, 6, 7], [1, 3, 5, 7, 9]] + result_data = [False, False, True, True, True] + + for variant, (input_hand, expected) in enumerate(zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', input_hand=input_hand, expected=expected): + actual_result = average_even_is_average_odd(input_hand) + error_message = (f'Called average_even_is_average_odd({input_hand}). ' + f'The function returned {actual_result}, but ' + f'the hand {"does" if expected else "does not"} ' + f'yield the same odd-even average.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=7) + def test_maybe_double_last(self): + """ + Test the maybe_double_last function to ensure it doubles the last card + if it is a Jack (11). + + :param self: The test case instance. + """ + input_data = [(1, 2, 11), (5, 9, 11), (5, 9, 10), (1, 2, 3), (1, 11, 8)] + result_data = [[1, 2, 22], [5, 9, 22], [5, 9, 10], [1, 2, 3], [1, 11, 8]] + + for variant, (hand, expected) in enumerate( + zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + hand=list(hand), + expected=expected): + actual_result = maybe_double_last(list(hand)) + error_message = (f'Called maybe_double_last({list(hand)}). ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} as the ' + f'maybe-doubled version of {list(hand)}.') + + self.assertEqual(actual_result, expected, msg=error_message) diff --git a/chaitanas-colossal-coaster/.exercism/config.json b/chaitanas-colossal-coaster/.exercism/config.json new file mode 100644 index 0000000..5374c4d --- /dev/null +++ b/chaitanas-colossal-coaster/.exercism/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "mohanrajanr", + "BethanyG" + ], + "contributors": [ + "BethanyG", + "valentin-p", + "pranasziaukas" + ], + "files": { + "solution": [ + "list_methods.py" + ], + "test": [ + "list_methods_test.py" + ], + "exemplar": [ + ".meta/exemplar.py" + ] + }, + "icon": "spiral-matrix", + "blurb": "Learn useful list methods helping Chaitana manage the lines for her colossal roller coaster." +} diff --git a/chaitanas-colossal-coaster/.exercism/metadata.json b/chaitanas-colossal-coaster/.exercism/metadata.json new file mode 100644 index 0000000..ad5441b --- /dev/null +++ b/chaitanas-colossal-coaster/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"chaitanas-colossal-coaster","id":"c7de01a0134641389ec4655d159c69e0","url":"https://exercism.org/tracks/python/exercises/chaitanas-colossal-coaster","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/chaitanas-colossal-coaster/HELP.md b/chaitanas-colossal-coaster/HELP.md new file mode 100644 index 0000000..0f5ca33 --- /dev/null +++ b/chaitanas-colossal-coaster/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 list_methods.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/chaitanas-colossal-coaster/HINTS.md b/chaitanas-colossal-coaster/HINTS.md new file mode 100644 index 0000000..df85ae9 --- /dev/null +++ b/chaitanas-colossal-coaster/HINTS.md @@ -0,0 +1,39 @@ +# Hints + +- Make sure you have a good understanding of how to create and update lists. +- The Python [documentation on `lists`][python lists] can be really helpful. +- The Python [tutorial section on `lists`][more on lists] is also a good resource. + +## 1. Add Me to the queue + +- An `if-else` statement can help you find which ticket type you are dealing with. +- You can then `append()` the person to the queue based on the ticket type. + +## 2. Where are my friends + +- You need to find the `index()` of the friend name from the queue. + +## 3. Can I please join them? + +- Since you know the `index()`, you can `insert()` the friend into the queue at that point. + +## 4. Mean person in the queue + +- You know the mean persons name, so you can `remove()` them from the queue. + +## 5. Namefellows + +- `count()`-ing the occurrences of the `name` in the queue could be a good strategy here. + +## 6. Remove the last person + +- Although you could `remove()` the person by name, `pop()`-ing them out might be quicker. + +## 7. Sort the Queue List + +- Don't forget that You need to avoid mutating the queue and losing its original order. +- Once you have a `copy()`, `sort()`-ing should be straightforward. +- We're looking for an _ascending_ sort, or _alphabetical from a-z_. + +[python lists]: https://docs.python.org/3.11/library/stdtypes.html#list +[more on lists]: https://docs.python.org/3.11/tutorial/datastructures.html#more-on-lists \ No newline at end of file diff --git a/chaitanas-colossal-coaster/README.md b/chaitanas-colossal-coaster/README.md new file mode 100644 index 0000000..d356814 --- /dev/null +++ b/chaitanas-colossal-coaster/README.md @@ -0,0 +1,419 @@ +# Chaitana's Colossal Coaster + +Welcome to Chaitana's Colossal Coaster on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +A [`list`][list] is a mutable collection of items in _sequence_. + Like most collections (_see the built-ins [`tuple`][tuple], [`dict`][dict] and [`set`][set]_), lists can hold reference to any (or multiple) data type(s) - including other lists. + Lists can be copied in whole or in part via [slice notation][slice notation] or through the use of `.copy()`. + Like any [sequence][sequence type], elements within `lists` are referenced by `0-based index` number from the left, or `-1-based index` number from the right. + +Lists support both [common][common sequence operations] and [mutable][mutable sequence operations] sequence operations such as `min()`/`max()`, `.index()`, `.append()` and `.reverse()`. + Elements inside a `list` can be iterated over using the `for item in ` construct. + `for index, item in enumerate()` can be used when both the element index and element value are needed. + +Python also provides many useful [list-methods][list-methods] for working with lists. + A selection of these `list methods` is covered below. + + +Note that when you manipulate a `list` with a `list-method`, **you alter the list** object that has been passed. + If you do not wish to mutate the original `list`, you will need to at least make a `shallow copy` of it via slice or `.copy()`. + + +## Adding Items + +To add an item to the end or "right-hand side" of an existing list, use `.append()`: + +```python +>>> numbers = [1, 2, 3] +>>> numbers.append(9) + +>>> numbers +[1, 2, 3, 9] +``` + +Rather than _appending_, `.insert()` gives you the ability to add the item to a _specific index_ in the list. +It takes 2 parameters: + +1. the `` at which you want the item to be inserted. +2. the `` to be inserted. + +**Note**: If the given `index` is 0, the item will be added to the start ("left-hand side") of the `list`. + If the supplied `index` is greater than the final `index` on the `list`, the item will be added in the final position -- the equivalent of using `.append()`. + + +```python +>>> numbers = [1, 2, 3] +>>> numbers.insert(0, -2) + +>>> numbers +[-2, 1, 2, 3] + +>>> numbers.insert(1, 0) + +>>> numbers +[-2, 0, 1, 2, 3] +``` + + +`.extend()` can be used to combine an existing list with the elements from another iterable (for example, a `set`, `tuple`, `str`, or `list`). + The iterable is _unpacked_ and elements are appended in order (_Using `.append()` in this circumstance would add the entire iterable as a **single item**._). + + +```python +>>> numbers = [1, 2, 3] +>>> other_numbers = [5, 6, 7] + +>>> numbers.extend(other_numbers) + +>>> numbers +[1, 2, 3, 5, 6, 7] + +>>> numbers.extend([8, 9]) + +>>> numbers +[1, 2, 3, 5, 6, 7, 8, 9] + +>>> numbers.append([8,9]) + +>>> numbers +[1, 2, 3, 5, 6, 7, 8, 9, [8, 9]] +``` + + +## Removing Items + +To delete an item from a list use `.remove()`, passing the item to be removed as an argument. + `.remove()` will throw a `ValueError` if the item is not present in the `list`. + + +```python +>>> numbers = [1, 2, 3] +>>> numbers.remove(2) + +>>> numbers +[1, 3] + +# Trying to remove a value that is not in the list throws a ValueError +>>> numbers.remove(0) +ValueError: list.remove(x): x not in list +``` + + +Alternatively, using the `.pop()` method will both remove **and** `return` an element for use. + + +`.pop()` takes one optional parameter: the `index` of the item to be removed and returned. + If the (optional) `index` argument is not specified, the final element of the `list` will be removed and returned. + If the `index` specified is higher than the final item `index`, an `IndexError` is raised. + + +```python +>>> numbers = [1, 2, 3] + +>>> numbers.pop(0) +1 + +>>> numbers +[2, 3] + +>>> numbers.pop() +3 + +>>> numbers +[2] + +>>> numbers.pop(1) +Traceback (most recent call last): + File "", line 1, in +IndexError: pop index out of range +``` + +All elements can be removed from a `list` with `list.clear()`. It doesn't take any parameters. + +```python +>>> numbers = [1, 2, 3] +>>> numbers.clear() + +>>> numbers +[] +``` + +## Reversing and reordering + +The `.reverse()` method will reverse the order of elements **in-place**. + + +```python +>>> numbers = [1, 2, 3] +>>> numbers.reverse() + +>>> numbers +[3, 2, 1] +``` + + +A list can be re-ordered _**in place**_ with the help of [`.sort()`][sort]. +Default sort order is _ascending_ from the left. +The Python docs offer [additional tips and techniques for sorting][sorting how to]. + +~~~~exercism/note + From 2002 to 2022, Python used an algorithm called [`Timsort`][timsort] internally to arrange lists, but switched to [`Powersort`][powersort] from `Python 3.11` onward. + +[powersort]: https://www.wild-inter.net/publications/munro-wild-2018 +[timsort]: https://en.wikipedia.org/wiki/Timsort +~~~~ + + +```python +>>> names = ["Tony", "Natasha", "Thor", "Bruce"] + +# The default sort order is *ascending*. +>>> names.sort() + +>>> names +["Bruce", "Natasha", "Thor", "Tony"] +``` + +If a _descending_ order is desired, pass the `reverse=True` argument: + +```python +>>> names = ["Tony", "Natasha", "Thor", "Bruce"] +>>> names.sort(reverse=True) + +>>> names +["Tony", "Thor", "Natasha", "Bruce"] +``` + +For cases where mutating the original list is undesirable, the built-in [`sorted()`][sorted] function can be used to return a sorted **copy**. + + +```python +>>> names = ["Tony", "Natasha", "Thor", "Bruce"] + +>>> sorted(names) +['Bruce', 'Natasha', 'Thor', 'Tony'] +``` + + +## Occurrences of an item in a list + +The number of occurrences of an element in a list can be calculated with the help of `list.count()`. + It takes the `item` to be counted as its argument and returns the total number of times that element appears in the `list`. + + +```python +>>> items = [1, 4, 7, 8, 2, 9, 2, 1, 1, 0, 4, 3] + +>>> items.count(1) +3 +``` + +## Finding the index of items + +`.index()` will return the `index` number of the _first occurrence_ of an item passed in. + If there are no occurrences, a `ValueError` is raised. + If the exact position of an item isn't needed, the built-in `in` operator is more efficient for checking if a list contains a given value. + + +Indexing is zero-based from the left, so the position of the "first" item is `0`. +Indexing will also work from the right, beginning with `-1`. + + +```python +>>> items = [7, 4, 1, 0, 2, 5] + +>>> items.index(4) +1 + +>>> items.index(10) +ValueError: 10 is not in list +``` + +`start` and `end` indices can also be provided to narrow the search to a specific section of the `list`: + +```python +>>> names = ["Tina", "Leo", "Thomas", "Tina", "Emily", "Justin"] + +>>> names.index("Tina") +0 + +>>> names.index("Tina", 2, 5) +3 +``` + +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[dict]: https://docs.python.org/3/library/stdtypes.html#dict +[list-methods]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists +[list]: https://docs.python.org/3/library/stdtypes.html#list +[mutable sequence operations]: https://docs.python.org/3/library/stdtypes.html#typesseq-mutable +[sequence type]: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range +[set]: https://docs.python.org/3/library/stdtypes.html#set +[slice notation]: https://docs.python.org/3/reference/expressions.html#slicings +[sort]: https://docs.python.org/3/library/stdtypes.html#list.sort +[sorted]: https://docs.python.org/3/library/functions.html#sorted +[sorting how to]: https://docs.python.org/3/howto/sorting.html +[tuple]: https://docs.python.org/3/library/stdtypes.html#tuple + +## Instructions + +Chaitana owns a very popular theme park. + She only has one ride in the very center of beautifully landscaped grounds: The Biggest Roller Coaster in the World(TM). + Although there is only this one attraction, people travel from all over the world and stand in line for hours for the opportunity to ride Chaitana's hypercoaster. + +There are two queues for this ride, each represented as a `list`: + +1. Normal Queue +2. Express Queue (_also known as the Fast-track_) - where people pay extra for priority access. + + +You have been asked to write some code to better manage the guests at the park. + You need to implement the following functions as soon as possible before the guests (and your boss, Chaitana!) get cranky. + Make sure you read carefully. + Some tasks ask that you change or update the existing queue, while others ask you to make a copy of it. + + +## 1. Add me to the queue + +Define the `add_me_to_the_queue()` function that takes 4 parameters `, , , ` and returns the appropriate queue updated with the person's name. + + +1. `` is an `int` with 1 == express_queue and 0 == normal_queue. +2. `` is the name (as a `str`) of the person to be added to the respective queue. + + +```python +>>> add_me_to_the_queue(express_queue=["Tony", "Bruce"], normal_queue=["RobotGuy", "WW"], ticket_type=1, person_name="RichieRich") +... +["Tony", "Bruce", "RichieRich"] + +>>> add_me_to_the_queue(express_queue=["Tony", "Bruce"], normal_queue=["RobotGuy", "WW"], ticket_type=0, person_name="HawkEye") +.... +["RobotGuy", "WW", "HawkEye"] +``` + +## 2. Where are my friends? + +One person arrived late at the park but wants to join the queue where their friends are waiting. + But they have no idea where their friends are standing and there isn't any phone reception to call them. + +Define the `find_my_friend()` function that takes 2 parameters `queue` and `friend_name` and returns the position in the queue of the person's name. + + +1. `` is the `list` of people standing in the queue. +2. `` is the name of the friend whose index (place in the queue) you need to find. + +Remember: Indexing starts at 0 from the left, and -1 from the right. + + +```python +>>> find_my_friend(queue=["Natasha", "Steve", "T'challa", "Wanda", "Rocket"], friend_name="Steve") +... +1 +``` + + +## 3. Can I please join them? + +Now that their friends have been found (in task #2 above), the late arriver would like to join them at their place in the queue. +Define the `add_me_with_my_friends()` function that takes 3 parameters `queue`, `index`, and `person_name`. + + +1. `` is the `list` of people standing in the queue. +2. `` is the position at which the new person should be added. +3. `` is the name of the person to add at the index position. + +Return the queue updated with the late arrivals name. + + +```python +>>> add_me_with_my_friends(queue=["Natasha", "Steve", "T'challa", "Wanda", "Rocket"], index=1, person_name="Bucky") +... +["Natasha", "Bucky", "Steve", "T'challa", "Wanda", "Rocket"] +``` + +## 4. Mean person in the queue + +You just heard from the queue that there is a really mean person shoving, shouting, and making trouble. + You need to throw that miscreant out for bad behavior! + + +Define the `remove_the_mean_person()` function that takes 2 parameters `queue` and `person_name`. + + +1. `` is the `list` of people standing in the queue. +2. `` is the name of the person that needs to be kicked out. + +Return the queue updated without the mean person's name. + +```python +>>> remove_the_mean_person(queue=["Natasha", "Steve", "Eltran", "Wanda", "Rocket"], person_name="Eltran") +... +["Natasha", "Steve", "Wanda", "Rocket"] +``` + + +## 5. Namefellows + +You may not have seen two unrelated people who look exactly the same, but you have _definitely_ seen unrelated people with the exact same name (_namefellows_)! + Today, it looks like there are a lot of them in attendance. + You want to know how many times a particular name occurs in the queue. + +Define the `how_many_namefellows()` function that takes 2 parameters `queue` and `person_name`. + +1. `` is the `list` of people standing in the queue. +2. `` is the name you think might occur more than once in the queue. + + +Return the number of occurrences of `person_name`, as an `int`. + + +```python +>>> how_many_namefellows(queue=["Natasha", "Steve", "Eltran", "Natasha", "Rocket"], person_name="Natasha") +... +2 +``` + +## 6. Remove the last person + +Sadly, it's overcrowded at the park today and you need to remove the last person in the normal line (_you will give them a voucher to come back in the fast-track on another day_). + You will have to define the function `remove_the_last_person()` that takes 1 parameter `queue`, which is the list of people standing in the queue. + +You should update the `list` and also `return` the name of the person who was removed, so you can write them a voucher. + + +```python +>>> remove_the_last_person(queue=["Natasha", "Steve", "Eltran", "Natasha", "Rocket"]) +... +'Rocket' +``` + +## 7. Sort the Queue List + +For administrative purposes, you need to get all the names in a given queue in alphabetical order. + + +Define the `sorted_names()` function that takes 1 argument, `queue`, (the `list` of people standing in the queue), and returns a `sorted` copy of the `list`. + + +```python +>>> sorted_names(queue=["Natasha", "Steve", "Eltran", "Natasha", "Rocket"]) +... +['Eltran', 'Natasha', 'Natasha', 'Rocket', 'Steve'] +``` + +## Source + +### Created by + +- @mohanrajanr +- @BethanyG + +### Contributed to by + +- @BethanyG +- @valentin-p +- @pranasziaukas \ No newline at end of file diff --git a/chaitanas-colossal-coaster/list_methods.py b/chaitanas-colossal-coaster/list_methods.py new file mode 100644 index 0000000..4e24015 --- /dev/null +++ b/chaitanas-colossal-coaster/list_methods.py @@ -0,0 +1,92 @@ +"""Functions to manage and organize queues at Chaitana's roller coaster.""" + + +def add_me_to_the_queue( + express_queue: list[str], + normal_queue: list[str], + ticket_type: int, + person_name: str, +) -> list[str]: + """ + Add a person to the 'express' or 'normal' queue depending on the ticket number. + + :param express_queue: list[str] - names in the Fast-track queue. + :param normal_queue: list[str] - names in the normal queue. + :param ticket_type: int - type of ticket. 1 = express, 0 = normal. + :param person_name: str - name of person to add to a queue. + :return: list - the (updated) queue the name was added to. + """ + if ticket_type: + express_queue.append(person_name) + return express_queue + + normal_queue.append(person_name) + return normal_queue + + +def find_my_friend(queue: list[str], friend_name: str) -> int: + """ + Search the queue for a name and return their queue position (index). + + :param queue: list - names in the queue. + :param friend_name: str - name of friend to find. + :return: int - index at which the friends name was found. + """ + return queue.index(friend_name) + + +def add_me_with_my_friends(queue: list[str], index: int, person_name: str) -> list[str]: + """ + Insert the late arrival's name at a specific index of the queue. + + :param queue: list - names in the queue. + :param index: int - the index at which to add the new name. + :param person_name: str - the name to add. + :return: list - queue updated with new name. + """ + queue.insert(index, person_name) + return queue + + +def remove_the_mean_person(queue: list[str], person_name: str) -> list[str]: + """ + Remove the mean person from the queue by the provided name. + + :param queue: list[str] - names in the queue. + :param person_name: str - name of mean person. + :return: list - queue update with the mean persons name removed. + """ + queue.remove(person_name) + return queue + + +def how_many_namefellows(queue: list[str], person_name: str) -> int: + """ + Count how many times the provided name appears in the queue. + + :param queue: list - names in the queue. + :param person_name: str - name you wish to count or track. + :return: int - the number of times the name appears in the queue. + """ + return queue.count(person_name) + + +def remove_the_last_person(queue: list[str]) -> str: + """ + Remove the person in the last index from the queue and return their name. + + :param queue: list - names in the queue. + :return: str - name that has been removed from the end of the queue. + """ + name: str = queue.pop() + return name + + +def sorted_names(queue: list[str]) -> list[str]: + """ + Sort the names in the queue in alphabetical order and return the result. + + :param queue: list - names in the queue. + :return: list - copy of the queue in alphabetical order. + """ + return sorted(queue) diff --git a/chaitanas-colossal-coaster/list_methods_test.py b/chaitanas-colossal-coaster/list_methods_test.py new file mode 100644 index 0000000..2d68b0b --- /dev/null +++ b/chaitanas-colossal-coaster/list_methods_test.py @@ -0,0 +1,426 @@ +# pylint: disable=C0301 +""" +Unit tests for queue management functions at Chaitana's roller coaster. + +This test suite, +- Validates correct functional behavior for basic queue operations (add, remove, insert, search, and sort) for both express and normal queues. +- Ensures that each function both returns the correct result and properly mutates or does not mutate the input lists as expected. +- Employs parameterized testing patterns, subTest blocks, and deep copies to safeguard against unintended mutation of shared state, ensuring accurate and isolated assertions. +- Uses unittest and pytest frameworks for flexible test discovery and marking (with @pytest.mark.task to delineate test responsibilities). + +Each function imported from `list_methods` is tested with a range of scenarios, including: +- Standard and edge inputs, +- In-place versus return-value mutation expectations, +- Validation that the queue's structure and contents match anticipated post-conditions. + +Users can run this file as a standard unittest/pytest module. Extend or edit test cases as new queue behaviors are implemented. +""" +import unittest +from copy import deepcopy +import pytest + + +from list_methods import ( + add_me_to_the_queue, + find_my_friend, + add_me_with_my_friends, + remove_the_mean_person, + how_many_namefellows, + remove_the_last_person, + sorted_names, +) + + +# pylint: disable=C0301 +class ListMethodsTest(unittest.TestCase): + """ + Unit test suite for list manipulation functions. + + Tests various utility methods defined in the list_methods module. + """ + + @pytest.mark.task(taskno=1) + def test_add_me_to_the_queue(self): + """ + Test the add_me_to_the_queue function to ensure it adds a person + to the correct queue based on ticket type and returns the updated queue. + + :param self: The test case instance. + """ + test_data = [ + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), + ['RobotGuy', 'WW', 'HawkEye']), + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), + ['Tony', 'Bruce', 'RichieRich']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), + ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), + ['Drax', 'Nebula', 'Gamora']), + ] + + for variant, (params, expected) in enumerate(test_data, start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + express_queue, normal_queue, ticket_type, person_name = deepcopy(params) + + with self.subTest(f'variation #{variant}', + params=params, + expected=expected): + actual_result = add_me_to_the_queue(*params) + + error_message = ( + f'\nCalled add_me_to_the_queue' + f'{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The function returned {actual_result},\n' + f' but the tests expected {expected} after {person_name} was added.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=1) + def test_add_me_to_the_queue_validate_queue(self): + """ + Test the add_me_to_the_queue function to validate that it mutates + and returns the correct queue based on ticket type. + + :param self: The test case instance. + """ + test_data = [ + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), + ['RobotGuy', 'WW', 'HawkEye']), + ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), + ['Tony', 'Bruce', 'RichieRich']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), + ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']), + ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), + ['Drax', 'Nebula', 'Gamora']), + ] + + for variant, (params, expected) in enumerate(test_data, start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + express_queue, normal_queue, ticket_type, person_name = deepcopy(params) + express, normal, ticket, name = params + + with self.subTest(f'variation #{variant}', + express=express, + normal=normal, + ticket=ticket, + name=name, + expected=expected): + + actual_result = add_me_to_the_queue(express, normal, ticket, name) + + if type == 1: + error_message = ( + f'\nCalled add_me_to_the_queue' + f'{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The queue == {express}, but the tests expected\n' + f'queue == {expected} after {person_name} was added.' + ) + + self.assertIs(actual_result, express, msg=error_message) + + if type == 0: + error_message = ( + f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n' + f'The queue == {normal}, but the tests expected \n' + f'queue == {expected} after {person_name} was added.' + ) + + self.assertIs(actual_result, normal, msg=error_message) + + @pytest.mark.task(taskno=2) + def test_find_my_friend(self): + """ + Test the find_my_friend function to ensure it returns the correct + index of the friend in the queue. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Natasha'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Steve'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Rocket'), + ] + + result_data = (0,1,4) + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = find_my_friend(*params) + error_message = ( + f'\nCalled find_my_friend{params}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when looking for\n' + f'{params[-1]} in the queue.' + ) + + self.assertIs(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_add_me_with_my_friends(self): + """ + Test the add_me_with_my_friends function to ensure it inserts the person + at the specified index and returns the updated queue. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 5, 'Bucky'), + ] + + result_data = [ + ['Bucky', 'Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Bucky', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket', 'Bucky'], + ] + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + queue, index, person_name = deepcopy(params) + + with self.subTest(f'variation #{variant}', params=params, expected=expected): + + actual_result = add_me_with_my_friends(*params) + error_message = ( + f'\nCalled add_me_with_my_friends{queue, index, person_name}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when adding\n' + f'{person_name} to position {index} in the queue.' + ) + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_add_me_with_my_friends_validate_queue(self): + """ + Test the add_me_with_my_friends function to validate that it mutates + the original queue and returns it. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'), + (['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 5, 'Bucky'), + ] + + result_data = [ + ['Bucky', 'Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Bucky', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket', 'Bucky'], + ] + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + start_queue, add_index, person_name = deepcopy(params) + queue, _, _ = params + + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = add_me_with_my_friends(*params) + error_message = ( + f'\nCalled add_me_with_my_friends{start_queue, add_index, person_name}.\n' + f'The function returned {actual_result},\n' + 'but the original queue was unmodified. The tests expected the \n' + f'*original* queue to be modified by adding "{person_name}".' + ) + + self.assertIs(actual_result, queue, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_remove_the_mean_person(self): + """ + Test the remove_the_mean_person function to ensure it removes the specified + person and returns the updated queue. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'), + (['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'), + (['Ultron', 'Natasha', 'Steve', 'Wanda', 'Rocket'], 'Steve'), + ] + + result_data = [ + ['Natasha', 'Steve', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Wanda', 'Ultron'], + ['Ultron', 'Natasha', 'Wanda', 'Rocket'], + ] + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + start_queue, person_name = deepcopy(params) + + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = remove_the_mean_person(*params) + error_message = ( + f'\nCalled remove_the_mean_person{start_queue, person_name}.\n' + f'The function returned {actual_result}, but\n' + f'the tests expected {expected} when removing\n' + f'{person_name} from the queue.' + ) + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_remove_the_mean_person_validate_queue(self): + """ + Test the remove_the_mean_person function to validate that it mutates + the original queue and returns it. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'), + (['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'), + (['Ultron', 'Natasha', 'Steve', 'Wanda', 'Rocket'], 'Steve'), + ] + + result_data = [ + ['Natasha', 'Steve', 'Wanda', 'Rocket'], + ['Natasha', 'Steve', 'Wanda', 'Ultron'], + ['Ultron', 'Natasha', 'Wanda', 'Rocket'], + ] + + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + start_queue, person_name = deepcopy(params) + queue, _ = params + + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = remove_the_mean_person(*params) + error_message = ( + f'\nCalled remove_the_mean_person{start_queue, person_name}.\n' + f'The function returned {actual_result}, queue == {queue}.\n' + f'But the tests expected queue == {expected} when removing\n' + f'{person_name}.' + ) + + self.assertIs(actual_result, queue, msg=error_message) + + + @pytest.mark.task(taskno=5) + def test_how_many_namefellows(self): + """ + Test the how_many_namefellows function to ensure it correctly counts + occurrences of a name in the queue. + + :param self: The test case instance. + """ + test_data = [(['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], 'Bucky'), + (['Natasha', 'Steve', 'Ultron', 'Rocket'], 'Natasha'), + (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], 'Natasha')] + + result_data = (0,1,2) + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = how_many_namefellows(*params) + + error_message = (f'Called how_many_namefellows{params}. ' + f'The function returned {actual_result}, but ' + f'The tests expected {expected} when counting ' + f'namefellows in the queue for {params[-1]}.') + + self.assertIs(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=6) + def test_remove_the_last_person(self): + """ + Test the remove_the_last_person function to ensure it removes and returns + the last person from the queue, mutating the queue. + + :param self: The test case instance. + """ + test_data = [ + (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], + ['Natasha', 'Steve', 'Ultron', 'Natasha'], 'Rocket'), + (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'], + ['Wanda', 'Natasha', 'Steve', 'Rocket'], 'Ultron'), + (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'], + ['Steve', 'Wanda', 'Rocket', 'Ultron'], 'Natasha') + ] + for variant, (queue, modified, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, modified=modified, expected=expected): + + # Deepcopy() is needed here because the task expects the input lists to be mutated. + # That mutation wrecks havoc with the verification and error messaging. + unmodified_queue = deepcopy(queue) + expected_result = expected + actual_result = remove_the_last_person(queue) + expected_queue = modified + + error_message = (f'\nCalled remove_the_last_person({unmodified_queue}).\n' + f'The function was expected to remove and return the name ' + f'"{expected_result}" ' + f'and change the queue to {expected_queue},\n' + f'but the name "{actual_result}" was returned and the queue == ' + f'{queue}.') + + self.assertEqual((actual_result, queue), (expected_result, expected_queue), msg=error_message) + + + @pytest.mark.task(taskno=7) + def test_sorted_names(self): + """ + Test the sorted_names function to ensure it returns a sorted copy + of the queue without mutating the original. + + :param self: The test case instance. + """ + test_data =( + (['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']), + (['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']), + (['Gamora', 'Loki', 'Tony', 'Peggy', 'Okoye'], ['Gamora', 'Loki', 'Okoye', 'Peggy', 'Tony']), + ) + + for variant, (queue, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, expected=expected): + actual_result = sorted_names(queue) + expected_result = expected + + error_message = (f'\nCalled sorted_names({queue}).\n' + f'The function returned {actual_result}, but \n' + f'the tests expect {expected_result}.') + + self.assertEqual(actual_result, expected_result, msg=error_message) + + @pytest.mark.task(taskno=7) + def test_sorted_names_validate_queue(self): + """ + Test the sorted_names function to validate that it does not mutate + the original queue. + + :param self: The test case instance. + """ + test_data = ( + (['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']), + (['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']), + (['Gamora', 'Loki', 'Tony', 'Peggy', 'Okoye'], ['Gamora', 'Loki', 'Okoye', 'Peggy', 'Tony']), + ) + + for variant, (queue, expected) in enumerate(test_data, start=1): + with self.subTest(f'variation #{variant}', queue=queue, expected=expected): + + # Deepcopy() is needed here because the input lists might be mutated. + # That mutation wrecks havoc with the verification and error messaging. + original_queue = deepcopy(queue) + actual_result = sorted_names(queue) + expected_result = expected + + error_message = (f'\nCalled sorted_names({original_queue}).\n' + f'The function returned {actual_result}, \n' + f'with a queue == {queue}.\n' + f'The tests expect {expected_result}, \n' + f'with a queue == {original_queue}.') + + self.assertIsNot(actual_result, queue, msg=error_message) diff --git a/collatz-conjecture/collatz_conjecture.py b/collatz-conjecture/collatz_conjecture.py index 7755bfc..a97aab0 100644 --- a/collatz-conjecture/collatz_conjecture.py +++ b/collatz-conjecture/collatz_conjecture.py @@ -13,6 +13,7 @@ """ +# pylint: disable=R0801 def steps(number: int) -> int: """ Return the number of steps it takes to reach 1 according to diff --git a/collatz-conjecture/collatz_conjecture_test.py b/collatz-conjecture/collatz_conjecture_test.py index 306e3db..1b2b3f5 100644 --- a/collatz-conjecture/collatz_conjecture_test.py +++ b/collatz-conjecture/collatz_conjecture_test.py @@ -1,3 +1,11 @@ +# pylint: disable=C0301 +""" +Unit tests for the steps function implementing the Collatz Conjecture. + +Tests include validation for zero, negative, even, odd values, and large numbers. +Ensures ValueError is raised for invalid input according to Collatz rules. +""" + # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/collatz-conjecture/canonical-data.json # File last updated on 2023-07-20 @@ -9,7 +17,10 @@ ) +# pylint: disable=C0116 class CollatzConjectureTest(unittest.TestCase): + """Unit tests for the functions related to the Collatz conjecture.""" + def test_zero_steps_for_one(self): self.assertEqual(steps(1), 0) @@ -26,10 +37,12 @@ def test_zero_is_an_error(self): with self.assertRaises(ValueError) as err: steps(0) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Only positive integers are allowed") + self.assertEqual(err.exception.args[0], + "Only positive integers are allowed") def test_negative_value_is_an_error(self): with self.assertRaises(ValueError) as err: steps(-15) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "Only positive integers are allowed") + self.assertEqual(err.exception.args[0], + "Only positive integers are allowed") diff --git a/currency-exchange/exchange.py b/currency-exchange/exchange.py index 2632aa2..25025a1 100644 --- a/currency-exchange/exchange.py +++ b/currency-exchange/exchange.py @@ -1,14 +1,15 @@ """ Functions for calculating steps in exchanging currency. -Python numbers documentation: https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex +Python numbers documentation: +https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex -Overview of exchanging currency when travelling: https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/ +Overview of exchanging currency when travelling: +https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/ """ -def exchange_money(budget: float, - exchange_rate: float) -> float: +def exchange_money(budget: float, exchange_rate: float) -> float: """ Return the value of the exchanged currency. @@ -22,8 +23,7 @@ def exchange_money(budget: float, return budget / exchange_rate -def get_change(budget: float, - exchanging_value: float) -> float: +def get_change(budget: float, exchanging_value: float) -> float: """ Return the amount of money that "is left" from the budget. @@ -31,13 +31,13 @@ def get_change(budget: float, :param exchanging_value: float - amount of your money you want to exchange now. :return: float - amount left of your starting currency after exchanging. """ - return budget - exchanging_value + return budget - exchanging_value # pylint: disable=R0801 -def get_value_of_bills(denomination: float, - number_of_bills: float) -> float: +def get_value_of_bills(denomination: float, number_of_bills: float) -> float: """ - Return only the total value of the bills (excluding fractional amounts) the booth would give back. + Return only the total value of the bills (excluding fractional amounts) + the booth would give back. The total you receive must be divisible by the value of one "bill" or unit, which can leave behind a fraction or remainder. @@ -49,8 +49,7 @@ def get_value_of_bills(denomination: float, return denomination * number_of_bills -def get_number_of_bills(amount: float, - denomination: int) -> int: +def get_number_of_bills(amount: float, denomination: int) -> int: """ Return the _number of currency bills_ that you can receive within the given _amount_. @@ -61,8 +60,7 @@ def get_number_of_bills(amount: float, return int(amount // denomination) -def get_leftover_of_bills(amount: float, - denomination: int) -> float: +def get_leftover_of_bills(amount: float, denomination: int) -> float: """ Return the _leftover amount_ that cannot be returned from your starting _amount_ given the denomination of bills. @@ -74,12 +72,13 @@ def get_leftover_of_bills(amount: float, return amount % denomination -def exchangeable_value(budget: float, - exchange_rate: float, - spread: int, - denomination: int) -> int: +# pylint: disable=R0801 +def exchangeable_value( + budget: float, exchange_rate: float, spread: int, denomination: int +) -> int: """ - Return the maximum value of the new currency after calculating the *exchange rate* plus the *spread*. + Return the maximum value of the new currency after calculating + the *exchange rate* plus the *spread*. :param budget: float - the amount of your money you are planning to exchange. :param exchange_rate: float - the unit value of the foreign currency. diff --git a/currency-exchange/exchange_test.py b/currency-exchange/exchange_test.py index fd3754c..7f1f495 100644 --- a/currency-exchange/exchange_test.py +++ b/currency-exchange/exchange_test.py @@ -1,3 +1,12 @@ +# pylint: disable=C0301 +""" +Unit tests for currency exchange functions. + +Tests cover calculations for currency exchange, change, bill values, bill counts, leftovers, and determining maximum currency exchangeable after applying spread and denomination constraints. + +Uses pytest and unittest for parametrized and task-specific testing. +""" + import unittest import pytest @@ -10,7 +19,10 @@ exchangeable_value) +# pylint: disable=C0116 class CurrencyExchangeTest(unittest.TestCase): + """Unit tests for currency exchange utility functions.""" + @pytest.mark.task(taskno=1) def test_exchange_money(self): test_data = [(100000, 0.8), (700000, 10.0)] @@ -133,7 +145,8 @@ def test_exchangeable_value(self): expected=expected): actual_result = exchangeable_value(budget, exchange_rate, spread, denomination) - error_message = (f'Called exchangeable_value{budget, exchange_rate, spread, denomination}. ' + error_message = (f'Called exchangeable_value' + f'{budget, exchange_rate, spread, denomination}. ' f'The function returned {actual_result}, but ' f'The tests expected {expected} as the maximum ' f'value of the new currency .') diff --git a/ghost-gobble-arcade-game/arcade_game.py b/ghost-gobble-arcade-game/arcade_game.py index eb5fe64..3cb4bc2 100644 --- a/ghost-gobble-arcade-game/arcade_game.py +++ b/ghost-gobble-arcade-game/arcade_game.py @@ -34,9 +34,9 @@ def lose(power_pellet_active: bool, touching_ghost: bool) -> bool: return touching_ghost and not power_pellet_active -def win(has_eaten_all_dots: bool, - power_pellet_active: bool, - touching_ghost: bool) -> bool: +def win( + has_eaten_all_dots: bool, power_pellet_active: bool, touching_ghost: bool +) -> bool: """ Trigger the victory event when all dots have been eaten. diff --git a/ghost-gobble-arcade-game/arcade_game_test.py b/ghost-gobble-arcade-game/arcade_game_test.py index 2ffdd3d..336eefd 100644 --- a/ghost-gobble-arcade-game/arcade_game_test.py +++ b/ghost-gobble-arcade-game/arcade_game_test.py @@ -1,10 +1,66 @@ +# pylint: disable=C0301, C0116 +""" + +This `arcade_game_test.py` file is a comprehensive set of unit tests +for the Pac-Man-style "ghost gobble arcade game" logic found in +`arcade_game.py`. The test class uses Python's `unittest` framework +(with `pytest` markers for task organization) and checks the +correctness of each function (`eat_ghost`, `score`, `lose`, `win`) +provided by the game logic module. + +Structure & Coverage + +1. **eat_ghost** +- Tests if Pac-Man can eat a ghost only when: + - A power pellet is active **and** + - He's touching a ghost. +- Includes positive (should eat) and negative (should not eat) scenarios. + +2. **score** +- Tests Pac-Man scores when: + - Touching a dot. + - Touching a power pellet. + - Not scoring when touching neither. + +3. **lose** +- Tests losing logic: + - Should lose if touching a ghost without a power pellet. + - Should *not* lose if touching a ghost *with* a power pellet active. + - Should *not* lose if not touching a ghost. + +4. **win** +- Tests winning the game: + - Wins if *all dots eaten* and hasn't lost (touching ghost *without* pellet). + - Doesn't win if all dots eaten *but* touching a ghost without a pellet. + - Wins if all dots eaten *and* touching a ghost *with* a pellet (i.e., didn't lose). + - Doesn't win if not all dots eaten. + +General Comments + +- The error messages are descriptive to aid debugging if an assertion fails. +- Each test calls the appropriate function with various inputs to check all critical edge and task-specific cases. +- Follows best practices for test case isolation and clarity. + +Final Note + +This suite provides full behavioral coverage of the gameplay logic as +specified in `arcade_game.py`. To run these tests, simply execute the +file with a compatible test runner (pytest or python -m unittest). +Make sure the tested functions are correctly imported from the +`arcade_game` module (as in your header). + +If you need any enhancements, parameterizations, or help troubleshooting +failing test cases, let me know! +""" import unittest import pytest from arcade_game import eat_ghost, score, lose, win class GhostGobbleGameTest(unittest.TestCase): - + """ + Unit tests for the Ghost Gobble arcade game functions using unittest framework. + """ @pytest.mark.task(taskno=1) def test_ghost_gets_eaten(self): actual_result = eat_ghost(True, True) diff --git a/grains/grains.py b/grains/grains.py index 277079e..b86139f 100644 --- a/grains/grains.py +++ b/grains/grains.py @@ -31,4 +31,4 @@ def total() -> int: :rtype: int """ # return sum(square(sqr) for sqr in range(1, 65)) - return 2 ** 64 - 1 + return 2**64 - 1 diff --git a/grains/grains_test.py b/grains/grains_test.py index 3c7f281..526319a 100644 --- a/grains/grains_test.py +++ b/grains/grains_test.py @@ -1,3 +1,12 @@ +# pylint: disable=C0301 +""" +Unit tests for the grains module. + +Tests the square and total functions to ensure correct calculations +of grains on each square, total grains, and proper error handling for +invalid input. +""" + # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grains/canonical-data.json # File last updated on 2023-09-27 @@ -10,7 +19,17 @@ ) +# pylint: disable=C0116 class GrainsTest(unittest.TestCase): + """ + Unit tests for the GrainsTest class, covering the square and + total functions from the grains module. + + Verifies correct grain counts for specific chessboard squares, + total grain calculation, and proper exception handling for + invalid input values. + """ + def test_grains_on_square_1(self): self.assertEqual(square(1), 1) @@ -48,19 +67,22 @@ def test_square_0_is_invalid(self): with self.assertRaises(ValueError) as err: square(0) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "square must be between 1 and 64") + self.assertEqual(err.exception.args[0], + "square must be between 1 and 64") def test_negative_square_is_invalid(self): with self.assertRaises(ValueError) as err: square(-1) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "square must be between 1 and 64") + self.assertEqual(err.exception.args[0], + "square must be between 1 and 64") def test_square_greater_than_64_is_invalid(self): with self.assertRaises(ValueError) as err: square(65) self.assertEqual(type(err.exception), ValueError) - self.assertEqual(err.exception.args[0], "square must be between 1 and 64") + self.assertEqual(err.exception.args[0], + "square must be between 1 and 64") def test_returns_the_total_number_of_grains_on_the_board(self): self.assertEqual(total(), 18446744073709551615) diff --git a/guidos-gorgeous-lasagna/lasagna.py b/guidos-gorgeous-lasagna/lasagna.py index 595a253..99722f8 100644 --- a/guidos-gorgeous-lasagna/lasagna.py +++ b/guidos-gorgeous-lasagna/lasagna.py @@ -8,11 +8,11 @@ of a module and its functions and/or classes. """ - EXPECTED_BAKE_TIME: int = 40 PREPARATION_TIME: int = 2 +# pylint: disable=R0801 def bake_time_remaining(elapsed_bake_time: int) -> int: """ Calculate the bake time remaining. @@ -44,8 +44,7 @@ def preparation_time_in_minutes(number_of_layers: int) -> int: return int(PREPARATION_TIME * number_of_layers) -def elapsed_time_in_minutes(number_of_layers: int, - elapsed_bake_time: int) -> int: +def elapsed_time_in_minutes(number_of_layers: int, elapsed_bake_time: int) -> int: """ Calculate elapsed time in minutes. @@ -54,10 +53,12 @@ def elapsed_time_in_minutes(number_of_layers: int, :param number_of_layers: The number of layers added to the lasagna. :type number_of_layers: int - :param elapsed_bake_time: The number of minutes the lasagna has spent baking in the oven already. + :param elapsed_bake_time: The number of minutes the lasagna has + spent baking in the oven already. :type elapsed_bake_time: int :return: Elapsed time in minutes. :rtype: int """ return preparation_time_in_minutes(number_of_layers=number_of_layers) + ( - EXPECTED_BAKE_TIME - bake_time_remaining(elapsed_bake_time)) + EXPECTED_BAKE_TIME - bake_time_remaining(elapsed_bake_time) + ) diff --git a/guidos-gorgeous-lasagna/lasagna_test.py b/guidos-gorgeous-lasagna/lasagna_test.py index 4066aa8..6a21967 100644 --- a/guidos-gorgeous-lasagna/lasagna_test.py +++ b/guidos-gorgeous-lasagna/lasagna_test.py @@ -1,9 +1,22 @@ +# pylint: disable=C0301 +""" +Unit tests for Guido's gorgeous lasagna module functions and constants. + +Covers presence and values of EXPECTED_BAKE_TIME, and correctness of +bake_time_remaining, preparation_time_in_minutes, and elapsed_time_in_minutes. +Also checks that all required docstrings are present. + +Raises informative import errors to help identify naming issues during testing. +""" + import unittest import pytest -# For this first exercise, it is really important to be clear about how we are importing names for tests. -# To that end, we are putting a try/catch around imports and throwing specific messages to help students -# decode that they need to create and title their constants and functions in a specific way. +# For this first exercise, it is really important to be clear about +# how we are importing names for tests.To that end, we are putting a +# try/catch around imports and throwing specific messages to help +# students decode that they need to create and title their constants +# and functions in a specific way. try: from lasagna import (EXPECTED_BAKE_TIME, bake_time_remaining, @@ -17,20 +30,26 @@ if 'EXPECTED_BAKE_TIME' in item_name: # pylint: disable=raise-missing-from - raise ImportError(f'\n\nMISSING CONSTANT --> \nWe can not find or import the constant {item_name} in your' - " 'lasagna.py' file.\nDid you misname or forget to define it?") from None - else: - item_name = item_name[:-1] + "()'" - # pylint: disable=raise-missing-from - raise ImportError("\n\nMISSING FUNCTION --> In your 'lasagna.py' file, we can not find or import the" - f' function named {item_name}. \nDid you misname or forget to define it?') from None + raise ImportError(f'\n\nMISSING CONSTANT --> \nWe can not find ' + f'or import the constant {item_name} in your' + " 'lasagna.py' file.\nDid you misname or forget " + "to define it?") from None + item_name = item_name[:-1] + "()'" + # pylint: disable=raise-missing-from + raise ImportError("\n\nMISSING FUNCTION --> In your 'lasagna.py' " + "file, we can not find or import the" + f' function named {item_name}. \nDid you misname ' + f'or forget to define it?') from None + +# pylint: disable=C0301, C0116 # Here begins the formal test cases for the exercise. class LasagnaTest(unittest.TestCase): + """Unit tests for lasagna-related functionality.""" @pytest.mark.task(taskno=1) - def test_EXPECTED_BAKE_TIME(self): + def test_expected_bake_time(self): failure_msg = 'Expected a constant of EXPECTED_BAKE_TIME with a value of 40.' self.assertEqual(EXPECTED_BAKE_TIME, 40, msg=failure_msg) @@ -39,11 +58,16 @@ def test_bake_time_remaining(self): input_data = [1, 2, 5, 10, 15, 23, 33, 39] result_data = [39, 38, 35, 30, 25, 17, 7, 1] - for variant, (time, expected) in enumerate(zip(input_data, result_data), start=1): - with self.subTest(f'variation #{variant}', time=time, expected=expected): + for variant, (time, expected) in enumerate( + zip(input_data, result_data), + start=1): + with self.subTest(f'variation #{variant}', + time=time, + expected=expected): actual_result = bake_time_remaining(time) - failure_msg = (f'Called bake_time_remaining({time}). ' - f'The function returned {actual_result}, but the tests ' + failure_msg = (f'Called bake_time_remaining({time}). ' + f'The function returned {actual_result}, ' + f'but the tests ' f'expected {expected} as the remaining bake time.') self.assertEqual(actual_result, expected, msg=failure_msg) @@ -53,11 +77,16 @@ def test_preparation_time_in_minutes(self): input_data = [1, 2, 5, 8, 11, 15] result_data = [2, 4, 10, 16, 22, 30] - for variant, (layers, expected) in enumerate(zip(input_data, result_data), start=1): - with self.subTest(f'variation #{variant}', layers=layers, expected=expected): + for variant, (layers, expected) in enumerate( + zip(input_data, result_data), + start=1): + with self.subTest(f'variation #{variant}', + layers=layers, + expected=expected): actual_result = preparation_time_in_minutes(layers) failure_msg = (f'Called preparation_time_in_minutes({layers}). ' - f'The function returned {actual_result}, but the tests ' + f'The function returned {actual_result}, ' + f'but the tests ' f'expected {expected} as the preparation time.') self.assertEqual(actual_result, expected, msg=failure_msg) @@ -68,8 +97,13 @@ def test_elapsed_time_in_minutes(self): time_data = (3, 7, 8, 4, 15, 20) result_data = [5, 11, 18, 20, 37, 50] - for variant, (layers, time, expected) in enumerate(zip(layer_data, time_data, result_data), start=1): - with self.subTest(f'variation #{variant}', layers=layers, time=time, expected=expected): + for variant, (layers, time, expected) in enumerate( + zip(layer_data, time_data, result_data), + start=1): + with self.subTest(f'variation #{variant}', + layers=layers, + time=time, + expected=expected): actual_result = elapsed_time_in_minutes(layers, time) failure_msg = (f'Called elapsed_time_in_minutes({layers}, {time}). ' f'The function returned {actual_result}, but the tests ' diff --git a/hello-world/hello_world.py b/hello-world/hello_world.py index d695ea1..318a3bc 100644 --- a/hello-world/hello_world.py +++ b/hello-world/hello_world.py @@ -1,2 +1,3 @@ +# pylint: disable=C0116, C0114 def hello(): - return 'Hello, World!' + return "Hello, World!" diff --git a/hello-world/hello_world_test.py b/hello-world/hello_world_test.py index c2d1154..811bf6a 100644 --- a/hello-world/hello_world_test.py +++ b/hello-world/hello_world_test.py @@ -1,3 +1,12 @@ +# pylint: disable=C0301 +""" +Unit tests for the 'hello' function in hello_world.py. + +Ensures the function returns 'Hello, World!' as expected. +Provides informative error messages if the function is +missing or incorrectly implemented. +""" + # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/hello-world/canonical-data.json # File last updated on 2023-07-19 @@ -24,8 +33,14 @@ ) from None +# pylint: disable=C0116 class HelloWorldTest(unittest.TestCase): + """ + Test case class for verifying HelloWorld functionality using unittest. + """ + def test_say_hi(self): - msg = ("\n\nThis test expects a return of the string 'Hello, World!' \nDid you use print('Hello, World!') by " - "mistake?") + msg = ("\n\nThis test expects a return of the string " + "'Hello, World!' \nDid you use print('Hello, World!') " + "by mistake?") self.assertEqual(hello(), "Hello, World!", msg=msg) diff --git a/leap/.exercism/config.json b/leap/.exercism/config.json new file mode 100644 index 0000000..2e838e9 --- /dev/null +++ b/leap/.exercism/config.json @@ -0,0 +1,38 @@ +{ + "authors": [], + "contributors": [ + "AnAccountForReportingBugs", + "behrtam", + "BethanyG", + "betojulio", + "cmccandless", + "cruxicheiros", + "Dog", + "fluxusfrequency", + "iandexter", + "ikhadykin", + "kytrinyx", + "lowks", + "N-Parsons", + "olufotebig", + "pheanex", + "sambryant4", + "sjakobi", + "tqa236", + "yawpitch" + ], + "files": { + "solution": [ + "leap.py" + ], + "test": [ + "leap_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Determine whether a given year is a leap year.", + "source": "CodeRanch Cattle Drive, Assignment 3", + "source_url": "https://web.archive.org/web/20240907033714/https://coderanch.com/t/718816/Leap" +} diff --git a/leap/.exercism/metadata.json b/leap/.exercism/metadata.json new file mode 100644 index 0000000..92bd007 --- /dev/null +++ b/leap/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"leap","id":"552c24d202a746c0a8ee434d79220370","url":"https://exercism.org/tracks/python/exercises/leap","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/leap/HELP.md b/leap/HELP.md new file mode 100644 index 0000000..13ed4ec --- /dev/null +++ b/leap/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 leap.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/leap/README.md b/leap/README.md new file mode 100644 index 0000000..73840d5 --- /dev/null +++ b/leap/README.md @@ -0,0 +1,53 @@ +# Leap + +Welcome to Leap on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Introduction + +A leap year (in the Gregorian calendar) occurs: + +- In every year that is evenly divisible by 4. +- Unless the year is evenly divisible by 100, in which case it's only a leap year if the year is also evenly divisible by 400. + +Some examples: + +- 1997 was not a leap year as it's not divisible by 4. +- 1900 was not a leap year as it's not divisible by 400. +- 2000 was a leap year! + +~~~~exercism/note +For a delightful, four-minute explanation of the whole phenomenon of leap years, check out [this YouTube video](https://www.youtube.com/watch?v=xX96xng7sAE). +~~~~ + +## Instructions + +Your task is to determine whether a given year is a leap year. + +## Source + +### Contributed to by + +- @AnAccountForReportingBugs +- @behrtam +- @BethanyG +- @betojulio +- @cmccandless +- @cruxicheiros +- @Dog +- @fluxusfrequency +- @iandexter +- @ikhadykin +- @kytrinyx +- @lowks +- @N-Parsons +- @olufotebig +- @pheanex +- @sambryant4 +- @sjakobi +- @tqa236 +- @yawpitch + +### Based on + +CodeRanch Cattle Drive, Assignment 3 - https://web.archive.org/web/20240907033714/https://coderanch.com/t/718816/Leap \ No newline at end of file diff --git a/leap/leap.py b/leap/leap.py new file mode 100644 index 0000000..2930657 --- /dev/null +++ b/leap/leap.py @@ -0,0 +1,25 @@ +"""Solution for Leap exercise.""" + + +def leap_year(year: int) -> bool: + """ + Determine whether a given year is a leap year. + + A leap year (in the Gregorian calendar) occurs: + - In every year that is evenly divisible by 4. + - Unless the year is evenly divisible by 100, + in which case it's only a leap year if the + year is also evenly divisible by 400. + + Some examples: + + - 1997 was not a leap year as it's not divisible by 4. + - 1900 was not a leap year as it's not divisible by 400. + - 2000 was a leap year! + + :param year: any year + :type year: int + :return: whether a given year is a leap year + :rtype: bool + """ + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) diff --git a/leap/leap_test.py b/leap/leap_test.py new file mode 100644 index 0000000..42e425a --- /dev/null +++ b/leap/leap_test.py @@ -0,0 +1,48 @@ +# pylint: disable=C0301 +""" +Unit tests for the leap_year function, verifying correct leap +year logic for various cases based on canonical Exercism +problem specifications. +""" + +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/leap/canonical-data.json +# File last updated on 2023-07-19 + +import unittest + +from leap import ( + leap_year, +) + + +# pylint: disable=C0116 +class LeapTest(unittest.TestCase): + """Unit tests for verifying leap year calculation functionality.""" + + def test_year_not_divisible_by_4_in_common_year(self): + self.assertIs(leap_year(2015), False) + + def test_year_divisible_by_2_not_divisible_by_4_in_common_year(self): + self.assertIs(leap_year(1970), False) + + def test_year_divisible_by_4_not_divisible_by_100_in_leap_year(self): + self.assertIs(leap_year(1996), True) + + def test_year_divisible_by_4_and_5_is_still_a_leap_year(self): + self.assertIs(leap_year(1960), True) + + def test_year_divisible_by_100_not_divisible_by_400_in_common_year(self): + self.assertIs(leap_year(2100), False) + + def test_year_divisible_by_100_but_not_by_3_is_still_not_a_leap_year(self): + self.assertIs(leap_year(1900), False) + + def test_year_divisible_by_400_is_leap_year(self): + self.assertIs(leap_year(2000), True) + + def test_year_divisible_by_400_but_not_by_125_is_still_a_leap_year(self): + self.assertIs(leap_year(2400), True) + + def test_year_divisible_by_200_not_divisible_by_400_in_common_year(self): + self.assertIs(leap_year(1800), False) diff --git a/little-sisters-vocab/strings.py b/little-sisters-vocab/strings.py index f94c355..419c4a4 100644 --- a/little-sisters-vocab/strings.py +++ b/little-sisters-vocab/strings.py @@ -8,7 +8,7 @@ def add_prefix_un(word: str) -> str: :param word: str - containing the root word. :return: str - of root word prepended with 'un'. """ - return f'un{word}' + return f"un{word}" def make_word_groups(vocab_words: list[str]) -> str: @@ -27,7 +27,7 @@ def make_word_groups(vocab_words: list[str]) -> str: For example: list('en', 'close', 'joy', 'lighten'), produces the following string: 'en :: enclose :: enjoy :: enlighten'. """ - return f' :: {vocab_words[0]}'.join(vocab_words) + return f" :: {vocab_words[0]}".join(vocab_words) def remove_suffix_ness(word: str) -> str: @@ -39,7 +39,7 @@ def remove_suffix_ness(word: str) -> str: For example: "heaviness" becomes "heavy", but "sadness" becomes "sad". """ - return f'{word[:-5]}y' if word[-5] == 'i' else word[:-4] + return f"{word[:-5]}y" if word[-5] == "i" else word[:-4] def adjective_to_verb(sentence: str, index: int) -> str: @@ -53,5 +53,5 @@ def adjective_to_verb(sentence: str, index: int) -> str: For example, ("It got dark as the sun set.", 2) becomes "darken". """ words: list[str] = sentence.split() - word: str = words[index].strip('.,!?;:') - return f'{word}en' + word: str = words[index].strip(".,!?;:") + return f"{word}en" diff --git a/little-sisters-vocab/strings_test.py b/little-sisters-vocab/strings_test.py index b13d4e9..1b2999c 100644 --- a/little-sisters-vocab/strings_test.py +++ b/little-sisters-vocab/strings_test.py @@ -1,3 +1,12 @@ +# pylint: disable=C0301 +""" +Unit tests for the string manipulation functions in the 'strings' module. + +Tests adding the 'un' prefix, transforming word groups with a prefix, removing +the '-ness' suffix, and converting adjectives to verbs within sentences. +Uses pytest markers for task organization and detailed error messages for clarity. +""" + import unittest import pytest from strings import (add_prefix_un, @@ -6,28 +15,41 @@ adjective_to_verb) +# pylint: disable=C0116 class LittleSistersVocabTest(unittest.TestCase): + """ + Unit tests for vocabulary string manipulation functions in + the 'strings' module. + """ @pytest.mark.task(taskno=1) def test_add_prefix_un(self): - input_data = ['happy', 'manageable', 'fold', 'eaten', 'avoidable', 'usual'] + input_data = ['happy', 'manageable', 'fold', + 'eaten', 'avoidable', 'usual'] result_data = [f'un{item}' for item in input_data] - for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1): - with self.subTest(f'variation #{variant}', word=word, expected=expected): - + for variant, (word, expected) in enumerate( + zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + word=word, + expected=expected): actual_result = add_prefix_un(word) error_message = (f'Called add_prefix_un("{word}"). ' - f'The function returned "{actual_result}", but the ' - f'tests expected "{expected}" after adding "un" as a prefix.') + f'The function returned "{actual_result}", ' + f'but the ' + f'tests expected "{expected}" after adding ' + f'"un" as a prefix.') self.assertEqual(actual_result, expected, msg=error_message) @pytest.mark.task(taskno=2) def test_make_word_groups_en(self): - input_data = ['en', 'circle', 'fold', 'close', 'joy', 'lighten', 'tangle', 'able', 'code', 'culture'] - expected = ('en :: encircle :: enfold :: enclose :: enjoy :: enlighten ::' - ' entangle :: enable :: encode :: enculture') + input_data = ['en', 'circle', 'fold', 'close', + 'joy', 'lighten', 'tangle', 'able', + 'code', 'culture'] + expected = ('en :: encircle :: enfold :: enclose :: ' + 'enjoy :: enlighten ::' + ' entangle :: enable :: encode :: enculture') actual_result = make_word_groups(input_data) error_message = (f'Called make_word_groups({input_data}). ' @@ -39,10 +61,12 @@ def test_make_word_groups_en(self): @pytest.mark.task(taskno=2) def test_make_word_groups_pre(self): - input_data = ['pre', 'serve', 'dispose', 'position', 'requisite', 'digest', - 'natal', 'addressed', 'adolescent', 'assumption', 'mature', 'compute'] - expected = ('pre :: preserve :: predispose :: preposition :: prerequisite :: ' - 'predigest :: prenatal :: preaddressed :: preadolescent :: preassumption :: ' + input_data = ['pre', 'serve', 'dispose', 'position', + 'requisite', 'digest', 'natal', 'addressed', + 'adolescent', 'assumption', 'mature', 'compute'] + expected = ('pre :: preserve :: predispose :: preposition :: ' + 'prerequisite :: predigest :: prenatal :: ' + 'preaddressed :: preadolescent :: preassumption :: ' 'premature :: precompute') actual_result = make_word_groups(input_data) @@ -55,7 +79,8 @@ def test_make_word_groups_pre(self): @pytest.mark.task(taskno=2) def test_make_word_groups_auto(self): - input_data = ['auto', 'didactic', 'graph', 'mate', 'chrome', 'centric', 'complete', + input_data = ['auto', 'didactic', 'graph', 'mate', + 'chrome', 'centric', 'complete', 'echolalia', 'encoder', 'biography'] expected = ('auto :: autodidactic :: autograph :: automate :: autochrome :: ' 'autocentric :: autocomplete :: autoecholalia :: autoencoder :: ' @@ -71,8 +96,9 @@ def test_make_word_groups_auto(self): @pytest.mark.task(taskno=2) def test_make_words_groups_inter(self): - input_data = ['inter', 'twine', 'connected', 'dependent', 'galactic', 'action', - 'stellar', 'cellular', 'continental', 'axial', 'operative', 'disciplinary'] + input_data = ['inter', 'twine', 'connected', 'dependent', + 'galactic', 'action', 'stellar', 'cellular', + 'continental', 'axial', 'operative', 'disciplinary'] expected = ('inter :: intertwine :: interconnected :: interdependent :: ' 'intergalactic :: interaction :: interstellar :: intercellular :: ' 'intercontinental :: interaxial :: interoperative :: interdisciplinary') @@ -87,16 +113,22 @@ def test_make_words_groups_inter(self): @pytest.mark.task(taskno=3) def test_remove_suffix_ness(self): - input_data = ['heaviness', 'sadness', 'softness', 'crabbiness', 'lightness', 'artiness', 'edginess'] - result_data = ['heavy', 'sad', 'soft', 'crabby', 'light', 'arty', 'edgy'] - - for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1): - with self.subTest(f'variation #{variant}', word=word, expected=expected): + input_data = ['heaviness', 'sadness', 'softness', + 'crabbiness', 'lightness', 'artiness', + 'edginess'] + result_data = ['heavy', 'sad', 'soft', 'crabby', + 'light', 'arty', 'edgy'] + + for variant, (word, expected) in enumerate( + zip(input_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + word=word, + expected=expected): actual_result = remove_suffix_ness(word) error_message = (f'Called remove_suffix_ness("{word}"). ' f'The function returned "{actual_result}", ' - f'but the tests expected "{expected}" after the ' - 'suffix was removed.') + f'but the tests expected "{expected}" ' + f'after the suffix was removed.') self.assertEqual(actual_result, expected, msg=error_message) @@ -113,13 +145,20 @@ def test_adjective_to_verb(self): 'The black oil got on the white dog.'] index_data = [-2, -1, 3, 3, -2, -3, 5, 2, 1] result_data = ['brighten', 'darken', 'harden', 'soften', - 'lighten', 'dampen', 'shorten', 'weaken', 'blacken'] - - for variant, (sentence, index, expected) in enumerate(zip(input_data, index_data, result_data), start=1): - with self.subTest(f'variation #{variant}', sentence=sentence, index=index, expected=expected): + 'lighten', 'dampen', 'shorten', 'weaken', + 'blacken'] + + for variant, (sentence, index, expected) in enumerate( + zip(input_data, index_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + sentence=sentence, + index=index, + expected=expected): actual_result = adjective_to_verb(sentence, index) - error_message = (f'Called adjective_to_verb("{sentence}", {index}). ' - f'The function returned "{actual_result}", but the tests ' + error_message = (f'Called adjective_to_verb("' + f'{sentence}", {index}). ' + f'The function returned "{actual_result}", ' + f'but the tests ' f'expected "{expected}" as the verb for ' f'the word at index {index}.') diff --git a/making-the-grade/.exercism/config.json b/making-the-grade/.exercism/config.json new file mode 100644 index 0000000..d9edd46 --- /dev/null +++ b/making-the-grade/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "mohanrajanr", + "BethanyG" + ], + "contributors": [ + "pranasziaukas" + ], + "files": { + "solution": [ + "loops.py" + ], + "test": [ + "loops_test.py" + ], + "exemplar": [ + ".meta/exemplar.py" + ] + }, + "icon": "grep", + "blurb": "Learn about loops by grading and organizing your students exam scores." +} diff --git a/making-the-grade/.exercism/metadata.json b/making-the-grade/.exercism/metadata.json new file mode 100644 index 0000000..cd84997 --- /dev/null +++ b/making-the-grade/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"making-the-grade","id":"914830bc9f4d490ab3f718915d588c13","url":"https://exercism.org/tracks/python/exercises/making-the-grade","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/making-the-grade/HELP.md b/making-the-grade/HELP.md new file mode 100644 index 0000000..6ac515f --- /dev/null +++ b/making-the-grade/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 loops.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/making-the-grade/HINTS.md b/making-the-grade/HINTS.md new file mode 100644 index 0000000..79e03af --- /dev/null +++ b/making-the-grade/HINTS.md @@ -0,0 +1,59 @@ +# Hints + +## General + +- [`while`][while-loops] loops are used for _indefinite_ (uncounted) iteration +- [`for`][for-loops] loops are used for _definite_, (counted) iteration. +- The keywords [`break` and `continue`][control flow] help customize loop behavior. +- [`range(, stop, )`][range] can be used to generate a sequence for a loop counter. +- The built-in [`enumerate()`][enumerate] will return (``, ``) pairs to iterate over. + +Also being familiar with the following can help with completing the tasks: + +- [`lists`][list]: indexing, nested lists, [`.append`][append and pop], [`.pop()`][append and pop]. +- [`str`][str]: `str()` constructor, using the `+` to concatenate strings, optionally, [`f-strings`][f-strings]. + +## 1. Rounding Scores + +- `While` loops will continue to execute until their test condition evaluates to `False`. +- `.pop()` will remove and return the last item in a `list`. +- Empty lists evaluate to `False` (most empty objects in Python are "Falsy") + +## 2. Non-Passing Students + +- There's no need to declare `loop` counters or `index` counters when iterating through an object using a `for` loop. +- A results counter does need to be set up and _incremented_ -- you'll want to `return` the count of non-passing students when the loop terminates. + +## 3. The "Best" + +- There's no need to declare `loop` counters or `index` counters when iterating through an object using a `for` loop. +- Having an empty `list` to add the "best" marks to is helpful here. +- `.append()` can help add things to the results `list`. + +## 4. Calculating Letter Grades + +- These are _lower thresholds_. The _lower threshold_ for a "D" is a score of **41**, since an "F" is **<= 40**. +- [`range()`][range] can be helpful here to generate a sequence with the proper "F" -> "A" increments. +- [`round()`][round] without parameters should round off increments nicely. +- As with "the best" task, `.append()` could be useful here to append items from `range()` into a results `list`. + +## 5. Matching Names to Scores + +- [`enumerate()`][enumerate] could be helpful here. +- If both lists are the same length and sorted the same way, could you use the `index` from one to retrieve a `value` from the other? + +## 6. A "Perfect" Score + +- There may be or may not be a student with a score of 100, and you can't return `[]` without checking **all** scores. +- The [`control flow`][control flow] statements `continue` and `break` may be useful here to move past unwanted values. + +[append and pop]: https://docs.python.org/3/tutorial/datastructures.html#more-on-lists +[control flow]: https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops +[enumerate]: https://docs.python.org/3/library/functions.html#enumerate +[f-strings]: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals +[for-loops]: https://docs.python.org/3/tutorial/controlflow.html#for-statements +[list]: https://docs.python.org/3/library/stdtypes.html#list +[range]: https://docs.python.org/3/tutorial/controlflow.html#the-range-function +[round]: https://docs.python.org/3/library/functions.html#round +[str]: https://docs.python.org/3/library/stdtypes.html#str +[while-loops]: https://docs.python.org/3/reference/compound_stmts.html#the-while-statement \ No newline at end of file diff --git a/making-the-grade/README.md b/making-the-grade/README.md new file mode 100644 index 0000000..fe60e12 --- /dev/null +++ b/making-the-grade/README.md @@ -0,0 +1,321 @@ +# Making the Grade + +Welcome to Making the Grade on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +Python has two looping constructs. +`while` loops for _indefinite_ (uncounted) iteration and `for` loops for _definite_, (counted) iteration. +The keywords `break`, `continue`, and `else` help customize loop behavior. +`range()` and `enumerate()` help with loop counting and indexing. + + +## While + +[`while`][while statement] loops will continue to execute as long as the `loop expression` or "test" evaluates to `True` in a [`boolean context`][truth value testing], terminating when it evaluates to `False`: + +```python + +# Lists are considered "truthy" in a boolean context if they +# contain one or more values, and "falsy" if they are empty. + +>>> placeholders = ["spam", "ham", "eggs", "green_spam", "green_ham", "green_eggs"] + +>>> while placeholders: +... print(placeholders.pop(0)) +... +'spam' +'ham' +'eggs' +'green_spam' +'green_ham' +'green_eggs' +``` + + +## For + +The basic [`for`][for statement] `loop` in Python is better described as a _`for each`_ which cycles through the values of any [iterable object][iterable], terminating when there are no values returned from calling [`next()`][next built-in]: + +```python + +>>> word_list = ["bird", "chicken", "barrel", "bongo"] + +>>> for word in word_list: +... if word.startswith("b"): +... print(f"{word.title()} starts with a B.") +... else: +... print(f"{word.title()} doesn't start with a B.") +... +'Bird starts with a B.' +'Chicken doesn\'t start with a B.' +'Barrel starts with a B.' +'Bongo starts with a B.' +``` + + +## Sequence Object range() + +When there isn't a specific `iterable` given, the special [`range()`][range] sequence is used as a loop counter. +`range()` requires an `int` before which to `stop` the sequence, and can optionally take `start` and `step` parameters. +If no `start` number is provided, the sequence will begin with 0. +`range()` objects are **lazy** (_values are generated on request_), support all [common sequence operations][common sequence operations], and take up a fixed amount of memory, no matter how long the sequence specified. + +```python +# Here we use range to produce some numbers, rather than creating a list of them in memory. +# The values will start with 1 and stop *before* 7 + +>>> for number in range(1, 7): +... if number % 2 == 0: +... print(f"{number} is even.") +... else: +... print(f"{number} is odd.") +'1 is odd.' +'2 is even.' +'3 is odd.' +'4 is even.' +'5 is odd.' +'6 is even.' + +# range() can also take a *step* parameter. +# Here we use range to produce only the "odd" numbers, starting with 3 and stopping *before* 15. + +>>> for number in range(3, 15, 2): +... if number % 2 == 0: +... print(f"{number} is even.") +... else: +... print(f"{number} is odd.") +... +'3 is odd.' +'5 is odd.' +'7 is odd.' +'9 is odd.' +'11 is odd.' +'13 is odd.' +``` + + +## Values and Indexes with enumerate() + +If both values and their indexes are needed, the built-in [`enumerate()`][enumerate] will return (`index`, `value`) pairs: + +```python + +>>> word_list = ["bird", "chicken", "barrel", "apple"] + +# *index* and *word* are the loop variables. +# Loop variables can be any valid python name. + +>>> for index, word in enumerate(word_list): +... if word.startswith("b"): +... print(f"{word.title()} (at index {index}) starts with a B.") +... else: +... print(f"{word.title()} (at index {index}) doesn't start with a B.") +... +'Bird (at index 0) starts with a B.' +'Chicken (at index 1) doesn\'t start with a B.' +'Barrel (at index 2) starts with a B.' +'Apple (at index 3) doesn\'t start with a B.' + + +# The same method can be used as a "lookup" for pairing items between two lists. +# Of course, if the lengths or indexes don't line up, this doesn't work. + +>>> word_list = ["cat", "chicken", "barrel", "apple", "spinach"] +>>> category_list = ["mammal", "bird", "thing", "fruit", "vegetable"] + +>>> for index, word in enumerate(word_list): +... print(f"{word.title()} is in category: {category_list[index]}.") +... +'Cat is in category: mammal.' +'Chicken is in category: bird.' +'Barrel is in category: thing.' +'Apple is in category: fruit.' +'Spinach is in category: vegetable.' +``` + + +## Altering Loop Behavior + +The [`continue`][continue statement] keyword can be used to skip forward to the next iteration cycle: + +```python +word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple", "bear"] + +# This will skip *bird*, at index 0 +for index, word in enumerate(word_list): + if index == 0: + continue + if word.startswith("b"): + print(f"{word.title()} (at index {index}) starts with a b.") + +'Barrel (at index 2) starts with a b.' +'Bongo (at index 3) starts with a b.' +'Bear (at index 6) starts with a b.' +``` + + +The [`break`][break statement] (_like in many C-related languages_) keyword can be used to stop the iteration and "break out" of the innermost enclosing `loop`: + +```python +>>> word_list = ["bird", "chicken", "barrel", "bongo", "sliver", "apple"] + +>>> for index, word in enumerate(word_list): +... if word.startswith("b"): +... print(f"{word.title()} (at index {index}) starts with a B.") +... elif word == "sliver": +... break +... else: +... print(f"{word.title()} doesn't start with a B.") +... print("loop broken.") +... +'Bird (at index 0) starts with a B.' +'Chicken doesn\'t start with a B.' +'Barrel (at index 2) starts with a B.' +'Bongo (at index 3) starts with a B.' +'loop broken.' +``` + +[break statement]: https://docs.python.org/3/reference/simple_stmts.html#the-break-statement +[common sequence operations]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations +[continue statement]: https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement +[enumerate]: https://docs.python.org/3/library/functions.html#enumerate +[for statement]: https://docs.python.org/3/reference/compound_stmts.html#for +[iterable]: https://docs.python.org/3/glossary.html#term-iterable +[next built-in]: https://docs.python.org/3/library/functions.html#next +[range]: https://docs.python.org/3/library/stdtypes.html#range +[truth value testing]: https://docs.python.org/3/library/stdtypes.html#truth-value-testing +[while statement]: https://docs.python.org/3/reference/compound_stmts.html#the-while-statement + +## Instructions + +You're a teaching assistant correcting student exams. +Keeping track of results manually is getting both tedious and mistake-prone. +You decide to make things a little more interesting by putting together some functions to count and calculate results for the class. + +## 1. Rounding Scores + +While you can give "partial credit" on exam questions, overall exam scores have to be `int`s. +So before you can do anything else with the class scores, you need to go through the grades and turn any `float` scores into `int`s. Lucky for you, Python has the built-in [`round()`][round] function you can use. + +Create the function `round_scores(student_scores)` that takes a `list` of `student_scores`. +This function should _consume_ the input `list` and `return` a new list with all the scores converted to `int`s. +The order of the scores in the resulting `list` is not important. + +```python +>>> student_scores = [90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3] +>>> round_scores(student_scores) +... +[40, 39, 95, 80, 25, 31, 70, 55, 40, 90] +``` + +## 2. Non-Passing Students + +As you were grading the exam, you noticed some students weren't performing as well as you had hoped. +But you were distracted, and forgot to note exactly _how many_ students. + +Create the function `count_failed_students(student_scores)` that takes a `list` of `student_scores`. +This function should count up the number of students who don't have passing scores and return that count as an integer. +A student needs a score greater than **40** to achieve a passing grade on the exam. + +```python +>>> count_failed_students(student_scores=[90,40,55,70,30,25,80,95,38,40]) +5 +``` + +## 3. The "Best" + +The teacher you're assisting wants to find the group of students who've performed "the best" on this exam. +What qualifies as "the best" fluctuates, so you need to find the student scores that are **greater than or equal to** the current threshold. + +Create the function `above_threshold(student_scores, threshold)` taking `student_scores` (a `list` of grades), and `threshold` (the "top score" threshold) as parameters. +This function should return a `list` of all scores that are `>=` to `threshold`. + +```python +>>> above_threshold(student_scores=[90,40,55,70,30,68,70,75,83,96], threshold=75) +[90,75,83,96] +``` + +## 4. Calculating Letter Grades + +The teacher you are assisting likes to assign letter grades as well as numeric scores. +Since students rarely score 100 on an exam, the "letter grade" lower thresholds are calculated based on the highest score achieved, and increment evenly between the high score and the failing threshold of **<= 40**. + +Create the function `letter_grades(highest)` that takes the "highest" score on the exam as an argument, and returns a `list` of lower score thresholds for each "American style" grade interval: `["D", "C", "B", "A"]`. + + +```python +"""Where the highest score is 100, and failing is <= 40. + "F" <= 40 + 41 <= "D" <= 55 + 56 <= "C" <= 70 + 71 <= "B" <= 85 + 86 <= "A" <= 100 +""" + +>>> letter_grades(highest=100) +[41, 56, 71, 86] + + +"""Where the highest score is 88, and failing is <= 40. + "F" <= 40 + 41 <= "D" <= 52 + 53 <= "C" <= 64 + 65 <= "B" <= 76 + 77 <= "A" <= 88 +""" + +>>> letter_grades(highest=88) +[41, 53, 65, 77] +``` + +## 5. Matching Names to Scores + +You have a list of exam scores in descending order, and another list of student names also sorted in descending order by their exam scores. +You would like to match each student name with their exam score and print out an overall class ranking. + +Create the function `student_ranking(student_scores, student_names)` with parameters `student_scores` and `student_names`. +Match each student name on the student_names `list` with their score from the student_scores `list`. +You can assume each argument `list` will be sorted from highest score(er) to lowest score(er). +The function should return a `list` of strings with the format `. : `. + +```python +>>> student_scores = [100, 99, 90, 84, 66, 53, 47] +>>> student_names = ['Joci', 'Sara','Kora','Jan','John','Bern', 'Fred'] +>>> student_ranking(student_scores, student_names) +... +['1. Joci: 100', '2. Sara: 99', '3. Kora: 90', '4. Jan: 84', '5. John: 66', '6. Bern: 53', '7. Fred: 47'] +``` + +## 6. A "Perfect" Score + +Although a "perfect" score of 100 is rare on an exam, it is interesting to know if at least one student has achieved it. + +Create the function `perfect_score(student_info)` with parameter `student_info`. +`student_info` is a `list` of lists containing the name and score of each student: `[["Charles", 90], ["Tony", 80]]`. +The function should `return` _the first_ `[, ]` pair of the student who scored 100 on the exam. + +If no 100 scores are found in `student_info`, an empty list `[]` should be returned. + +```python +>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80], ["Alex", 100]]) +["Alex", 100] + +>>> perfect_score(student_info=[["Charles", 90], ["Tony", 80]]) +[] +``` + +[round]: https://docs.python.org/3/library/functions.html#round + +## Source + +### Created by + +- @mohanrajanr +- @BethanyG + +### Contributed to by + +- @pranasziaukas \ No newline at end of file diff --git a/making-the-grade/loops.py b/making-the-grade/loops.py new file mode 100644 index 0000000..3889505 --- /dev/null +++ b/making-the-grade/loops.py @@ -0,0 +1,87 @@ +"""Functions for organizing and calculating student exam scores.""" + + +def round_scores(student_scores: list) -> list: + """ + Round all provided student scores. + + :param student_scores: list - float or int of student exam scores. + :return: list - student scores *rounded* to nearest integer value. + """ + return [round(score) for score in student_scores] + + +def count_failed_students(student_scores: list) -> int: + """ + Count the number of failing students out of the group provided. + + :param student_scores: list - containing int student scores. + :return: int - count of student scores at or below 40. + """ + return len([score for score in student_scores if score <= 40.0]) # pylint: disable=R0801 + + +def above_threshold(student_scores: list, threshold: int) -> list: + """ + Filter out above threshold scores. + + Determine how many of the provided student scores were 'the best' + based on the provided threshold. + + :param student_scores: list - of integer scores. + :param threshold: int - threshold to cross to be the "best" score. + :return: list - of integer scores that are at or above the "best" threshold. + """ + return [score for score in student_scores if score >= threshold] # pylint: disable=R0801 + + +def letter_grades(highest: int) -> list: + """ + Create a list of grade thresholds based on the provided highest grade. + + :param highest: int - value of highest exam score. + :return: list - of lower threshold scores for each D-A letter grade interval. + For example, where the highest score is 100, and failing is <= 40, + The result would be [41, 56, 71, 86]: + + 41 <= "D" <= 55 + 56 <= "C" <= 70 + 71 <= "B" <= 85 + 86 <= "A" <= 100 + """ + interval = (highest - 40) // 4 + return [41 + i * interval for i in range(4)] + + +# pylint: disable=R0801 +def student_ranking(student_scores: list, student_names: list) -> list[str]: + """ + Organize the student's rank, name, and grade information in descending order. + + :param student_scores: list - of scores in descending order. + :param student_names: list - of string names by exam score in descending order. + :return: list - of strings in format [". : "]. + """ + return [ + f"{i}. {name}: {score}" + for i, score, name in zip( + range(1, len(student_scores) + 1), student_scores, student_names + ) + ] + + +# pylint: disable=R0801 +def perfect_score(student_info: list) -> list: + """ + Create a list that contains the name and grade of the first + student to make a perfect score on the exam. + + :param student_info: list - of [, ] lists. + :return: list - first `[, 100]` or `[]` if no + student score of 100 is found. + """ + for student, score in student_info: + if score == 100: + return [student, score] + + return [] diff --git a/making-the-grade/loops_test.py b/making-the-grade/loops_test.py new file mode 100644 index 0000000..47d6a34 --- /dev/null +++ b/making-the-grade/loops_test.py @@ -0,0 +1,189 @@ +# pylint: disable=C0301 +""" +Test suite for validating functions from the 'loops' module related +to student exam score calculations, including rounding scores, +counting failed students, filtering scores above a threshold, +generating letter grade cutoffs, ranking students, and identifying +perfect scores. +""" + +import unittest +import pytest + +from loops import ( + round_scores, + count_failed_students, + above_threshold, + letter_grades, + student_ranking, + perfect_score) + + +# pylint: disable=C0116 +class MakingTheGradeTest(unittest.TestCase): + """ + Test case class for verifying functions in the making-the-grade module. + """ + + @pytest.mark.task(taskno=1) + def test_round_scores(self): + + # Because we the input list can be mutated, the test data has + # been created as tuples, which we then convert to a list when + # the test runs. This makes accurate error messages easier to create. + test_data = [tuple(), + (.5,), + (1.5,), + (90.33, 40.5, 55.44, 70.05, 30.55, 25.45, + 80.45, 95.3, 38.7, 40.3), + (50, 36.03, 76.92, 40.7, 43, 78.29, 63.58, + 91, 28.6, 88.0)] + result_data = [[], + [0], + [2], + [90, 40, 55, 70, 31, 25, 80, 95, 39, 40], + [50, 36, 77, 41, 43, 78, 64, 91, 29, 88]] + + for variant, (student_scores, expected) in enumerate( + zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + student_scores=student_scores, + expected=expected): + + # Because the test_input is a tuple, it has to be converted + # to a list for the function call. + actual_result = round_scores(list(student_scores)) + error_message = (f'Called round_scores({list(student_scores)}). ' + f'The function returned {sorted(actual_result)} ' + f'after sorting, but ' + f'the tests expected {sorted(expected)} after ' + f'sorting. ' + f'One or more scores were rounded incorrectly.') + + # everything is sorted for easier comparison. + self.assertEqual( + sorted(actual_result), + sorted(expected), + msg=error_message) + + @pytest.mark.task(taskno=2) + def test_count_failed_students(self): + test_data = [[89, 85, 42, 57, 90, 100, 95, 48, 70, 96], + [40, 40, 35, 70, 30, 41, 90]] + result_data = [0,4] + + for variant, (student_scores, expected) in enumerate( + zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + student_scores=student_scores, + expected=expected): + + actual_result = count_failed_students(student_scores) + error_message = (f'Called count_failed_students({student_scores}). ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} for the ' + 'number of students who failed.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=3) + def test_above_threshold(self): + test_data = [([40, 39, 95, 80, 25, 31, 70, 55, 40, 90], 98), + ([88, 29, 91, 64, 78, 43, 41, 77, 36, 50], 80), + ([100, 89], 100), + ([88, 29, 91, 64, 78, 43, 41, 77, 36, 50], 78), + ([], 80)] + + result_data = [[], + [88, 91], + [100], + [88, 91, 78], + []] + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = above_threshold(*params) + error_message = (f'Called above_threshold{params}. ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} for the ' + 'scores that are above the threshold.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=4) + def test_letter_grades(self): + test_data = [100, 97, 85, 92, 81] + + result_data = [[41, 56, 71, 86], + [41, 55, 69, 83], + [41, 52, 63, 74], + [41, 54, 67, 80], + [41, 51, 61, 71]] + + for variant, (highest, expected) in enumerate( + zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', + highest=highest, + expected=expected): + actual_result = letter_grades(highest) + error_message = (f'Called letter_grades({highest}). ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} for the ' + 'letter grade cutoffs.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=5) + def test_student_ranking(self): + test_data = [([82], ['Betty']), + ([88, 73], ['Paul', 'Ernest']), + ([100, 98, 92, 86, 70, 68, 67, 60], + ['Rui', 'Betty', 'Joci', 'Yoshi', 'Kora', 'Bern', 'Jan', 'Rose'])] + + result_data = [['1. Betty: 82'], + ['1. Paul: 88', '2. Ernest: 73'], + ['1. Rui: 100', '2. Betty: 98', '3. Joci: 92', '4. Yoshi: 86', + '5. Kora: 70', '6. Bern: 68', '7. Jan: 67', '8. Rose: 60']] + + for variant, (params, expected) in enumerate(zip(test_data, result_data), start=1): + with self.subTest(f'variation #{variant}', params=params, expected=expected): + actual_result = student_ranking(*params) + error_message = (f'Called student_ranking{params}. ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} for the ' + 'student rankings.') + + self.assertEqual(actual_result, expected, msg=error_message) + + @pytest.mark.task(taskno=6) + def test_perfect_score(self): + test_data = [ + [['Joci', 100], ['Vlad', 100], ['Raiana', 100], + ['Alessandro', 100]], + [['Jill', 30], ['Paul', 73]], + [], + [['Rui', 60], ['Joci', 58], ['Sara', 91], ['Kora', 93], + ['Alex', 42], + ['Jan', 81], ['Lilliana', 40], ['John', 60], ['Bern', 28], + ['Vlad', 55]], + + [['Yoshi', 52], ['Jan', 86], ['Raiana', 100], ['Betty', 60], + ['Joci', 100], ['Kora', 81], ['Bern', 41], ['Rose', 94]] + ] + + + result_data = [['Joci', 100],[], [], [], ['Raiana', 100]] + + for variant, (student_info, expected) in enumerate( + zip(test_data, result_data), start=1): + + with self.subTest(f'variation #{variant}', + student_info=student_info, + expected=expected): + actual_result = perfect_score(student_info) + error_message = (f'Called perfect_score({student_info}). ' + f'The function returned {actual_result}, but ' + f'the tests expected {expected} for the ' + 'first "perfect" score.') + + self.assertEqual(actual_result, expected, msg=error_message) diff --git a/meltdown-mitigation/conditionals.py b/meltdown-mitigation/conditionals.py index 87b831b..e26fa94 100644 --- a/meltdown-mitigation/conditionals.py +++ b/meltdown-mitigation/conditionals.py @@ -1,8 +1,10 @@ """Functions to prevent a nuclear meltdown.""" +# pylint: disable=C0301 def is_criticality_balanced(temperature, neutrons_emitted) -> bool: - """Verify criticality is balanced. + """ + Verify criticality is balanced. :param temperature: int or float - temperature value in kelvin. :param neutrons_emitted: int or float - number of neutrons emitted per second. @@ -13,11 +15,16 @@ def is_criticality_balanced(temperature, neutrons_emitted) -> bool: - The number of neutrons emitted per second is greater than 500. - The product of temperature and neutrons emitted per second is less than 500000. """ - return temperature < 800 and neutrons_emitted > 500 and (temperature * neutrons_emitted) < 500000 + return ( + temperature < 800 + and neutrons_emitted > 500 + and (temperature * neutrons_emitted) < 500000 + ) def reactor_efficiency(voltage, current, theoretical_max_power) -> str: - """Assess reactor efficiency zone. + """ + Assess reactor efficiency zone. :param voltage: int or float - voltage value. :param current: int or float - current value. @@ -36,20 +43,23 @@ def reactor_efficiency(voltage, current, theoretical_max_power) -> str: where generated power = voltage * current """ generated_power = voltage * current - efficiency = (generated_power/theoretical_max_power)*100 + efficiency = (generated_power / theoretical_max_power) * 100 if efficiency < 30: - return 'black' - elif 30 <= efficiency < 60: - return 'red' - elif 60 <= efficiency < 80: - return 'orange' + return "black" + + if 30 <= efficiency < 60: + return "red" - return 'green' + if 60 <= efficiency < 80: + return "orange" + + return "green" def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str: - """Assess and return status code for the reactor. + """ + Assess and return status code for the reactor. :param temperature: int or float - value of the temperature in kelvin. :param neutrons_produced_per_second: int or float - neutron flux. @@ -64,8 +74,9 @@ def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str: thr_percent = threshold / 100 if thr_percent - 10 <= current_state <= thr_percent + 10: - return 'NORMAL' - elif current_state < thr_percent - 10: - return 'LOW' + return "NORMAL" + + if current_state < thr_percent - 10: + return "LOW" - return 'DANGER' + return "DANGER" diff --git a/meltdown-mitigation/conditionals_test.py b/meltdown-mitigation/conditionals_test.py index 5e48ca3..f0a4b8e 100644 --- a/meltdown-mitigation/conditionals_test.py +++ b/meltdown-mitigation/conditionals_test.py @@ -1,3 +1,11 @@ +# pylint: disable=C0301 +""" +Unit tests for the meltdown mitigation functions, verifying +correct behavior for criticality balance, reactor efficiency, +and fail safe status using a variety of boundary and +representative input values. +""" + import unittest import pytest from conditionals import (is_criticality_balanced, @@ -5,16 +13,16 @@ fail_safe) +# pylint: disable=C0116 class MeltdownMitigationTest(unittest.TestCase): - """Test cases for Meltdown mitigation exercise. - """ + """Test cases for Meltdown mitigation exercise.""" @pytest.mark.task(taskno=1) def test_is_criticality_balanced(self): - """Testing border cases around typical points. + """ + Testing border cases around typical points. T, n == (800, 500), (625, 800), (500, 1000), etc. - """ test_data = ((750, 650, True), (799, 501, True), (500, 600, True), @@ -26,13 +34,18 @@ def test_is_criticality_balanced(self): for variant, data in enumerate(test_data, start=1): temp, neutrons_emitted, expected = data - with self.subTest(f'variation #{variant}', temp=temp, neutrons_emitted=neutrons_emitted, expected=expected): + with self.subTest(f'variation #{variant}', + temp=temp, + neutrons_emitted=neutrons_emitted, + expected=expected): # pylint: disable=assignment-from-no-return actual_result = is_criticality_balanced(temp, neutrons_emitted) - failure_message = (f'Called is_criticality_balanced({temp}, {neutrons_emitted}). ' + failure_message = (f'Called is_criticality_balanced(' + f'{temp}, {neutrons_emitted}). ' f' The function returned {actual_result}, ' - f'but the test expected {expected} as the return value.') + f'but the test expected {expected} as the ' + f'return value.') self.assertEqual(actual_result, expected, failure_message) @@ -49,12 +62,16 @@ def test_reactor_efficiency(self): for variant, data in enumerate(test_data, start=1): current, expected = data - with self.subTest(f'variation #{variant}', voltage=voltage, current=current, - theoretical_max_power=theoretical_max_power, expected=expected): + with self.subTest(f'variation #{variant}', + voltage=voltage, + current=current, + theoretical_max_power=theoretical_max_power, + expected=expected): # pylint: disable=assignment-from-no-return actual_result = reactor_efficiency(voltage, current, theoretical_max_power) - failure_message =(f'Called reactor_efficiency({voltage}, {current}, {theoretical_max_power}). ' + failure_message =(f'Called reactor_efficiency(' + f'{voltage}, {current}, {theoretical_max_power}). ' f'The function returned {actual_result}, ' f'but the test expected {expected} as the return value.') @@ -70,12 +87,15 @@ def test_fail_safe(self): (400, 'LOW'), (1101, 'DANGER'), (1200, 'DANGER')) for variant, (neutrons_per_second, expected) in enumerate(test_data, start=1): - with self.subTest(f'variation #{variant}', temp=temp, neutrons_per_second=neutrons_per_second, + with self.subTest(f'variation #{variant}', + temp=temp, + neutrons_per_second=neutrons_per_second, threshold=threshold, expected=expected): # pylint: disable=assignment-from-no-return actual_result = fail_safe(temp, neutrons_per_second, threshold) - failure_message = (f'Called fail_safe({temp}, {neutrons_per_second}, {threshold}). ' + failure_message = (f'Called fail_safe(' + f'{temp}, {neutrons_per_second}, {threshold}). ' f'The function returned {actual_result}, ' f'but the test expected {expected} as the return value.') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8418471 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[tool.ruff] +# Exclude commonly ignored directories +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "tests/", + "*_test.py", + "test_*.py", + "solutions/" +] + +# Line length and indentation settings, same as Black +line-length = 88 +indent-width = 4 + +# Set target version to Python 3.12 +target-version = "py312" + +[tool.ruff.lint] +# Enable Pyflakes (F) and a subset of pycodestyle (E) codes by default +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fixes for all enabled rules when --fix is provided +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Enable formatter settings, similar to Black +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +# Auto-formatting of code examples in docstrings is currently disabled by default +docstring-code-format = false +docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 015de27..44bbf70 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/solutions/cpp/hello-world/1/hello_world.cpp b/solutions/cpp/hello-world/1/hello_world.cpp new file mode 100644 index 0000000..142ad1f --- /dev/null +++ b/solutions/cpp/hello-world/1/hello_world.cpp @@ -0,0 +1,16 @@ +#include "hello_world.h" + +// Use everything from the 'std' namespace. +// This lets us write 'string' instead of 'std::string'. +using namespace std; + +namespace hello_world { + +// Define the function itself. This could have also been written as: +// std::string hello_world::hello() +string hello() { + // Return the string we need. + return "Hello, World!"; +} + +} // namespace hello_world diff --git a/solutions/cpp/leap/1/leap.cpp b/solutions/cpp/leap/1/leap.cpp new file mode 100644 index 0000000..9d9d360 --- /dev/null +++ b/solutions/cpp/leap/1/leap.cpp @@ -0,0 +1,5 @@ +#include "leap.h" + +namespace leap { + +} // namespace leap diff --git a/solutions/cpp/leap/2/leap.cpp b/solutions/cpp/leap/2/leap.cpp new file mode 100644 index 0000000..ed6fdab --- /dev/null +++ b/solutions/cpp/leap/2/leap.cpp @@ -0,0 +1,15 @@ +#include "leap.h" + +namespace leap { + bool is_leap_year(int year) { + + //on every year that is evenly divisible by 4 + if (year % 4 == 0) { + return true; + } + else { + return false; + } + + } +} // namespace leap diff --git a/solutions/cpp/leap/3/leap.h b/solutions/cpp/leap/3/leap.h new file mode 100644 index 0000000..85341a5 --- /dev/null +++ b/solutions/cpp/leap/3/leap.h @@ -0,0 +1,8 @@ +#if !defined(LEAP_H) +#define LEAP_H + +namespace leap { + bool is_leap_year(int year); +} // namespace leap + +#endif // LEAP_H \ No newline at end of file diff --git a/solutions/cpp/leap/4/leap.cpp b/solutions/cpp/leap/4/leap.cpp new file mode 100644 index 0000000..ed6fdab --- /dev/null +++ b/solutions/cpp/leap/4/leap.cpp @@ -0,0 +1,15 @@ +#include "leap.h" + +namespace leap { + bool is_leap_year(int year) { + + //on every year that is evenly divisible by 4 + if (year % 4 == 0) { + return true; + } + else { + return false; + } + + } +} // namespace leap diff --git a/solutions/cpp/leap/5/leap.cpp b/solutions/cpp/leap/5/leap.cpp new file mode 100644 index 0000000..c37abd6 --- /dev/null +++ b/solutions/cpp/leap/5/leap.cpp @@ -0,0 +1,28 @@ +#include "leap.h" + +namespace leap { + //The tricky thing here is that a leap year in the Gregorian calendar occurs: + bool is_leap_year(int year) { + + //on every year that is evenly divisible by 4 + if (year % 4 == 0) { + + //except every year that is evenly divisible by 100 + if (year % 100 == 0) { + + //unless the year is also evenly divisible by 400 + if (year % 400 == 0) { + return true; + } + return false; + } + else { + return true; + }; + } + else { + return false; + } + + } +} // namespace leap diff --git a/solutions/cpp/leap/6/leap.cpp b/solutions/cpp/leap/6/leap.cpp new file mode 100644 index 0000000..3a0d4ae --- /dev/null +++ b/solutions/cpp/leap/6/leap.cpp @@ -0,0 +1,12 @@ +#include "leap.h" + +namespace leap { + //The tricky thing here is that a leap year in the Gregorian calendar occurs: + bool is_leap_year(int year) { + + //on every year that is evenly divisible by 4 + //except every year that is evenly divisible by 100 + //unless the year is also evenly divisible by 400 + return (year % 4 == 0 && year % 100 != 0) || (year % 100 == 0 && year % 400 == 0); + } +} // namespace leap diff --git a/solutions/cpp/leap/6/leap.h b/solutions/cpp/leap/6/leap.h new file mode 100644 index 0000000..85341a5 --- /dev/null +++ b/solutions/cpp/leap/6/leap.h @@ -0,0 +1,8 @@ +#if !defined(LEAP_H) +#define LEAP_H + +namespace leap { + bool is_leap_year(int year); +} // namespace leap + +#endif // LEAP_H \ No newline at end of file diff --git a/solutions/cpp/leap/7/leap.cpp b/solutions/cpp/leap/7/leap.cpp new file mode 100644 index 0000000..92756b7 --- /dev/null +++ b/solutions/cpp/leap/7/leap.cpp @@ -0,0 +1,12 @@ +#include "leap.h" + +namespace leap { + //The tricky thing here is that a leap year in the Gregorian calendar occurs: + bool is_leap_year(int year) { + + //on every year that is evenly divisible by 4 + //except every year that is evenly divisible by 100 + //unless the year is also evenly divisible by 400 + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + } +} // namespace leap diff --git a/solutions/cpp/leap/7/leap.h b/solutions/cpp/leap/7/leap.h new file mode 100644 index 0000000..85341a5 --- /dev/null +++ b/solutions/cpp/leap/7/leap.h @@ -0,0 +1,8 @@ +#if !defined(LEAP_H) +#define LEAP_H + +namespace leap { + bool is_leap_year(int year); +} // namespace leap + +#endif // LEAP_H \ No newline at end of file diff --git a/solutions/javascript/hello-world/1/hello-world.js b/solutions/javascript/hello-world/1/hello-world.js new file mode 100644 index 0000000..5fec175 --- /dev/null +++ b/solutions/javascript/hello-world/1/hello-world.js @@ -0,0 +1,8 @@ +// +// This is only a SKELETON file for the 'Hello World' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +export function hello() { + return 'Hello, World!'; +} diff --git a/solutions/python/armstrong-numbers/1/armstrong_numbers.py b/solutions/python/armstrong-numbers/1/armstrong_numbers.py new file mode 100644 index 0000000..0a14cd6 --- /dev/null +++ b/solutions/python/armstrong-numbers/1/armstrong_numbers.py @@ -0,0 +1,18 @@ +"""Armstrong Numbers.""" + + +def is_armstrong_number(number: int) -> bool: + """ + Test if "Armstrong Number". + + An Armstrong number is a number that is the sum of its own digits + each raised to the power of the number of digits. + + :param number: any integer number + :type number: int + :return: if "Armstrong Number" + :rtype: bool + """ + str_num: str = str(number) + n: int = len(str_num) + return number == sum(int(char) ** n for char in str_num) diff --git a/solutions/python/armstrong-numbers/2/armstrong_numbers.py b/solutions/python/armstrong-numbers/2/armstrong_numbers.py new file mode 100644 index 0000000..648d086 --- /dev/null +++ b/solutions/python/armstrong-numbers/2/armstrong_numbers.py @@ -0,0 +1,27 @@ +"""Armstrong Numbers.""" + + +def is_armstrong_number(number: int) -> bool: + """ + Test if "Armstrong Number". + + An Armstrong number is a number that is the sum of its own digits + each raised to the power of the number of digits. + + :param number: any integer number + :type number: int + :return: if "Armstrong Number" + :rtype: bool + + Examples: + >>> is_armstrong_number(153) + True + >>> is_armstrong_number(10) + False + """ + if number < 0: + raise ValueError("Only non-negative integers are allowed") + + str_num: str = str(number) + n: int = len(str_num) + return number == sum(int(char) ** n for char in str_num) diff --git a/solutions/python/black-jack/1/black_jack.py b/solutions/python/black-jack/1/black_jack.py new file mode 100644 index 0000000..c252b88 --- /dev/null +++ b/solutions/python/black-jack/1/black_jack.py @@ -0,0 +1,110 @@ +""" +Functions to help play and score a game of blackjack. + +How to play blackjack: https://bicyclecards.com/how-to-play/blackjack/ +"Standard" playing cards: https://en.wikipedia.org/wiki/Standard_52-card_deck +""" + + +def value_of_card(card) -> int: + """ + Determine the scoring value of a card. + + :param card: str - given card. + :return: int - value of a given card. See below for values. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. + """ + if card in 'JKQ': + return 10 + elif card == 'A': + return 1 + + return int(card) + + +def higher_card(card_one, card_two) -> str | tuple[str, str]: + """ + Determine which card has a higher value in the hand. + + :param card_one, card_two: str - cards dealt in hand. See below for values. + :return: str or tuple - resulting Tuple contains both cards if they are of equal value. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 1 + 3. '2' - '10' = numerical value. + """ + if value_of_card(card_one) == value_of_card(card_two): + return card_one, card_two + elif value_of_card(card_one) > value_of_card(card_two): + return card_one + + return card_two + + +def value_of_ace(card_one, card_two) -> int: + """ + Calculate the most advantageous value for the ace card. + + :param card_one, card_two: str - card dealt. See below for values. + :return: int - either 1 or 11 value of the upcoming ace card. + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. + """ + total: int = value_of_card(card_one) + value_of_card(card_two) + # Hint: if we already have an ace in hand, then the value for the upcoming ace would be 1. + if card_one == 'A' or card_two == 'A': + return 1 + # The value of the hand with the ace needs to be as high as possible without going over 21. + elif 21 - total >= 11: + return 11 + + return 1 + + +def is_blackjack(card_one, card_two) -> bool: + """ + Determine if the hand is a 'natural' or 'blackjack'. + + :param card_one, card_two: str - card dealt. See below for values. + :return: bool - is the hand is a blackjack (two cards worth 21). + + 1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10 + 2. 'A' (ace card) = 11 (if already in hand) + 3. '2' - '10' = numerical value. + """ + # If a player is dealt an ace (A) and a ten-card (10, K, Q, or J) + # as their first two cards, then the player has a score of 21. + if card_one == 'A' and card_two in ('J', 'Q', 'K', '10'): + return True + elif card_two == 'A' and card_one in ('J', 'Q', 'K', '10'): + return True + + return False + + +def can_split_pairs(card_one, card_two) -> bool: + """ + Determine if a player can split their hand into two hands. + + :param card_one, card_two: str - cards dealt. + :return: bool - can the hand be split into two pairs? (i.e. cards are of the same value). + """ + if value_of_card(card_one) == value_of_card(card_two): + return True + + return False + + +def can_double_down(card_one, card_two) -> bool: + """ + Determine if a blackjack player can place a double down bet. + + :param card_one, card_two: str - first and second cards in hand. + :return: bool - can the hand can be doubled down? (i.e. totals 9, 10 or 11 points). + """ + return 9 <= value_of_card(card_one) + value_of_card(card_two) <= 11 diff --git a/solutions/python/collatz-conjecture/1/collatz_conjecture.py b/solutions/python/collatz-conjecture/1/collatz_conjecture.py new file mode 100644 index 0000000..1b7ce6e --- /dev/null +++ b/solutions/python/collatz-conjecture/1/collatz_conjecture.py @@ -0,0 +1,39 @@ +""" +Collatz Conjecture. + +The rules were deceptively simple. Pick any positive integer. + +If it's even, divide it by 2. +If it's odd, multiply it by 3 and add 1. +Then, repeat these steps with the result, continuing indefinitely. + +Curious, you picked number 12 to test and began the journey: + +12 ➜ 6 ➜ 3 ➜ 10 ➜ 5 ➜ 16 ➜ 8 ➜ 4 ➜ 2 ➜ 1 +""" + + +def steps(number: int) -> int | ValueError: + """ + Return the number of steps it takes to reach 1 according to + the rules of the Collatz Conjecture. + + :param number: any positive integer + :type number: int + :return: number of steps it takes to reach 1 + :rtype: int + """ + if number < 1: + raise ValueError("Only positive integers are allowed") + + n_steps: int = 0 + while number > 1: + # If it's even, divide it by 2 + if number % 2 == 0: + number = number / 2 + # If it's odd, multiply it by 3 and add 1 + else: + number = (number * 3) + 1 + n_steps += 1 + + return n_steps diff --git a/solutions/python/collatz-conjecture/2/collatz_conjecture.py b/solutions/python/collatz-conjecture/2/collatz_conjecture.py new file mode 100644 index 0000000..7755bfc --- /dev/null +++ b/solutions/python/collatz-conjecture/2/collatz_conjecture.py @@ -0,0 +1,41 @@ +""" +Collatz Conjecture. + +The rules were deceptively simple. Pick any positive integer. + +If it's even, divide it by 2. +If it's odd, multiply it by 3 and add 1. +Then, repeat these steps with the result, continuing indefinitely. + +Curious, you picked number 12 to test and began the journey: + +12 ➜ 6 ➜ 3 ➜ 10 ➜ 5 ➜ 16 ➜ 8 ➜ 4 ➜ 2 ➜ 1 +""" + + +def steps(number: int) -> int: + """ + Return the number of steps it takes to reach 1 according to + the rules of the Collatz Conjecture. + + :param number: any positive integer + :type number: int + :return: number of steps it takes to reach 1 + :rtype: int + """ + if number < 1: + raise ValueError("Only positive integers are allowed") + + n_steps: int = 0 + while number > 1: + # If it's even, divide it by 2 + if number % 2 == 0: + # Switch to integer division + # keeps everything as int and avoids precision issues + number = number // 2 + # If it's odd, multiply it by 3 and add 1 + else: + number = (number * 3) + 1 + n_steps += 1 + + return n_steps diff --git a/solutions/python/currency-exchange/1/exchange.py b/solutions/python/currency-exchange/1/exchange.py new file mode 100644 index 0000000..a0eeeac --- /dev/null +++ b/solutions/python/currency-exchange/1/exchange.py @@ -0,0 +1,97 @@ +""" +Functions for calculating steps in exchanging currency. + +Python numbers documentation: +https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex + +Overview of exchanging currency when travelling: +https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/ +""" + + +def exchange_money(budget: float, + exchange_rate: float) -> float: + """ + Return the value of the exchanged currency. + + Note: If your currency is USD and you want to exchange USD for EUR with + an exchange rate of `1.20`, then `1.20 USD == 1 EUR`. + + :param budget: float - amount of money you are planning to exchange. + :param exchange_rate: float - unit value of the foreign currency. + :return: float - exchanged value of the foreign currency you can receive. + """ + return budget / exchange_rate + + +def get_change(budget: float, + exchanging_value: float) -> float: + """ + Return the amount of money that "is left" from the budget. + + :param budget: float - amount of money you own. + :param exchanging_value: float - amount of your money you want to exchange now. + :return: float - amount left of your starting currency after exchanging. + """ + return budget - exchanging_value + + +def get_value_of_bills(denomination: float, + number_of_bills: float) -> float: + """ + Return only the total value of the bills (excluding fractional amounts) + the booth would give back. + + The total you receive must be divisible by the value of one "bill" or unit, + which can leave behind a fraction or remainder. + + :param denomination: int - the value of a bill. + :param number_of_bills: int - total number of bills. + :return: int - calculated value of the bills. + """ + return denomination * number_of_bills + + +def get_number_of_bills(amount: float, + denomination: int) -> int: + """ + Return the _number of currency bills_ that you can receive within the given _amount_. + + :param amount: float - the total starting value. + :param denomination: int - the value of a single bill. + :return: int - number of bills that can be obtained from the amount. + """ + return int(amount // denomination) + + +def get_leftover_of_bills(amount: float, + denomination: int) -> float: + """ + Return the _leftover amount_ that cannot be returned from your starting _amount_ + given the denomination of bills. + + :param amount: float - the total starting value. + :param denomination: int - the value of a single bill. + :return: float - the amount that is "leftover", given the current denomination. + """ + return amount % denomination + + +def exchangeable_value(budget: float, + exchange_rate: float, + spread: int, + denomination: int) -> int: + """ + Return the maximum value of the new currency after calculating + the *exchange rate* plus the *spread*. + + :param budget: float - the amount of your money you are planning to exchange. + :param exchange_rate: float - the unit value of the foreign currency. + :param spread: int - percentage that is taken as an exchange fee. + :param denomination: int - the value of a single bill. + :return: int - maximum value you can get. + """ + total_exchange_rate = exchange_rate + (exchange_rate * spread / 100) + foreign_currency = exchange_money(budget, total_exchange_rate) + bills = get_number_of_bills(foreign_currency, denomination) + return int(get_value_of_bills(bills, denomination)) diff --git a/solutions/python/ghost-gobble-arcade-game/1/arcade_game.py b/solutions/python/ghost-gobble-arcade-game/1/arcade_game.py new file mode 100644 index 0000000..eb5fe64 --- /dev/null +++ b/solutions/python/ghost-gobble-arcade-game/1/arcade_game.py @@ -0,0 +1,48 @@ +"""Functions for implementing the rules of the classic arcade game Pac-Man.""" + + +def eat_ghost(power_pellet_active: bool, touching_ghost: bool) -> bool: + """ + Verify that Pac-Man can eat a ghost if he is empowered by a power pellet. + + :param power_pellet_active: bool - does the player have an active power pellet? + :param touching_ghost: bool - is the player touching a ghost? + :return: bool - can a ghost be eaten? + """ + return power_pellet_active and touching_ghost + + +def score(touching_power_pellet: bool, touching_dot: bool) -> bool: + """ + Verify that Pac-Man has scored when a power pellet or dot has been eaten. + + :param touching_power_pellet: bool - is the player touching a power pellet? + :param touching_dot: bool - is the player touching a dot? + :return: bool - has the player scored or not? + """ + return touching_power_pellet or touching_dot + + +def lose(power_pellet_active: bool, touching_ghost: bool) -> bool: + """ + Trigger the game loop to end (GAME OVER) when Pac-Man touches a ghost without his power pellet. + + :param power_pellet_active: bool - does the player have an active power pellet? + :param touching_ghost: bool - is the player touching a ghost? + :return: bool - has the player lost the game? + """ + return touching_ghost and not power_pellet_active + + +def win(has_eaten_all_dots: bool, + power_pellet_active: bool, + touching_ghost: bool) -> bool: + """ + Trigger the victory event when all dots have been eaten. + + :param has_eaten_all_dots: bool - has the player "eaten" all the dots? + :param power_pellet_active: bool - does the player have an active power pellet? + :param touching_ghost: bool - is the player touching a ghost? + :return: bool - has the player won the game? + """ + return has_eaten_all_dots and not lose(power_pellet_active, touching_ghost) diff --git a/solutions/python/grains/1/grains.py b/solutions/python/grains/1/grains.py new file mode 100644 index 0000000..f59f376 --- /dev/null +++ b/solutions/python/grains/1/grains.py @@ -0,0 +1,36 @@ +"""Grains.""" + + +def square(number) -> int: + """ + Calculate the number of grains on a given square. + + One grain on the first square of a chessboard, with the number + of grains doubling on each successive square. + + :param number: A chessboard square + :type number: int + :return: number of grains on a given square. + :rtype: int + """ + if number <= 0 or number > 64: + raise ValueError(f"square must be between 1 and 64") + + if number == 1: + return 1 + + return 2 ** (number - 1) + + +def total() -> int: + """ + Calculate the total number of grains on the chessboard. + + A chessboard has 64 squares. Square 1 has one grain, + square 2 has two grains, square 3 has four grains, and so on, + doubling each time. + + :return: the total number of grains on the chessboard. + :rtype: int + """ + return sum(square(sqr) for sqr in range(1, 65)) diff --git a/solutions/python/grains/2/grains.py b/solutions/python/grains/2/grains.py new file mode 100644 index 0000000..277079e --- /dev/null +++ b/solutions/python/grains/2/grains.py @@ -0,0 +1,34 @@ +"""Grains.""" + + +def square(number) -> int: + """ + Calculate the number of grains on a given square. + + One grain on the first square of a chessboard, with the number + of grains doubling on each successive square. + + :param number: A chessboard square + :type number: int + :return: number of grains on a given square. + :rtype: int + """ + if number <= 0 or number > 64: + raise ValueError("square must be between 1 and 64") + + return 2 ** (number - 1) + + +def total() -> int: + """ + Calculate the total number of grains on the chessboard. + + A chessboard has 64 squares. Square 1 has one grain, + square 2 has two grains, square 3 has four grains, and so on, + doubling each time. + + :return: the total number of grains on the chessboard. + :rtype: int + """ + # return sum(square(sqr) for sqr in range(1, 65)) + return 2 ** 64 - 1 diff --git a/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py b/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py new file mode 100644 index 0000000..a69c70b --- /dev/null +++ b/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py @@ -0,0 +1,64 @@ +""" +Functions used in preparing Guido's gorgeous lasagna. + +Learn about Guido, the creator of the Python language: +https://en.wikipedia.org/wiki/Guido_van_Rossum + +This is a module docstring, used to describe the functionality +of a module and its functions and/or classes. +""" + + +EXPECTED_BAKE_TIME: int = 40 +PREPARATION_TIME: int = 2 + + +def bake_time_remaining(elapsed_bake_time: int) -> int: + """ + Calculate the bake time remaining. + + Function that takes the actual minutes the lasagna has been in the oven as + an argument and returns how many minutes the lasagna still needs to bake + based on the `EXPECTED_BAKE_TIME`. + + :param elapsed_bake_time: int - baking time already elapsed. + :return: int - remaining bake time (in minutes) derived from 'EXPECTED_BAKE_TIME'. + """ + return EXPECTED_BAKE_TIME - elapsed_bake_time + + +def preparation_time_in_minutes(number_of_layers: int) -> int: + """ + Calculate preparation time in minutes + + Takes the `number_of_layers` you want to add to the lasagna as an argument and + returns how many minutes you would spend making them. + + Assume each layer takes 2 minutes to prepare. + + :param number_of_layers: Number of layers you want to add to the lasagna. + :type number_of_layers: int + :return: Preparation time in minutes + :rtype: int + """ + return int(PREPARATION_TIME * number_of_layers) + + +def elapsed_time_in_minutes(number_of_layers: int, + elapsed_bake_time: int) -> int: + """ + Calculate elapsed time in minutes. + + Return the total minutes you have been in the kitchen cooking — your preparation time layering + + the time the lasagna has spent baking in the oven. + + :param number_of_layers: The number of layers added to the lasagna. + :type number_of_layers: int + :param elapsed_bake_time: The number of minutes the lasagna has spent + baking in the oven already. + :type elapsed_bake_time: int + :return: Elapsed time in minutes. + :rtype: int + """ + return preparation_time_in_minutes(number_of_layers=number_of_layers) + ( + EXPECTED_BAKE_TIME - bake_time_remaining(elapsed_bake_time)) diff --git a/solutions/python/hello-world/1/hello_world.py b/solutions/python/hello-world/1/hello_world.py new file mode 100644 index 0000000..d695ea1 --- /dev/null +++ b/solutions/python/hello-world/1/hello_world.py @@ -0,0 +1,2 @@ +def hello(): + return 'Hello, World!' diff --git a/solutions/python/leap/1/leap.py b/solutions/python/leap/1/leap.py new file mode 100644 index 0000000..dd1abb3 --- /dev/null +++ b/solutions/python/leap/1/leap.py @@ -0,0 +1,32 @@ +"""Solution for Leap exercise.""" + + +def leap_year(year: int) -> bool: + """ + Determine whether a given year is a leap year. + + A leap year (in the Gregorian calendar) occurs: + - In every year that is evenly divisible by 4. + - Unless the year is evenly divisible by 100, + in which case it's only a leap year if the + year is also evenly divisible by 400. + + Some examples: + + - 1997 was not a leap year as it's not divisible by 4. + - 1900 was not a leap year as it's not divisible by 400. + - 2000 was a leap year! + + :param year: any year + :type year: int + :return: whether a given year is a leap year + :rtype: bool + """ + if year % 4 == 0: + if year % 100 == 0: + if year % 400 == 0: + return True + return False + return True + + return False diff --git a/solutions/python/leap/2/leap.py b/solutions/python/leap/2/leap.py new file mode 100644 index 0000000..2930657 --- /dev/null +++ b/solutions/python/leap/2/leap.py @@ -0,0 +1,25 @@ +"""Solution for Leap exercise.""" + + +def leap_year(year: int) -> bool: + """ + Determine whether a given year is a leap year. + + A leap year (in the Gregorian calendar) occurs: + - In every year that is evenly divisible by 4. + - Unless the year is evenly divisible by 100, + in which case it's only a leap year if the + year is also evenly divisible by 400. + + Some examples: + + - 1997 was not a leap year as it's not divisible by 4. + - 1900 was not a leap year as it's not divisible by 400. + - 2000 was a leap year! + + :param year: any year + :type year: int + :return: whether a given year is a leap year + :rtype: bool + """ + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) diff --git a/solutions/python/little-sisters-vocab/1/strings.py b/solutions/python/little-sisters-vocab/1/strings.py new file mode 100644 index 0000000..3a97ce5 --- /dev/null +++ b/solutions/python/little-sisters-vocab/1/strings.py @@ -0,0 +1,57 @@ +"""Functions for creating, transforming, and adding prefixes to strings.""" + + +def add_prefix_un(word: str) -> str: + """ + Take the given word and add the 'un' prefix. + + :param word: str - containing the root word. + :return: str - of root word prepended with 'un'. + """ + return f'un{word}' + + +def make_word_groups(vocab_words: list) -> str: + """ + Transform a list containing a prefix and words into a string with the prefix + followed by the words with prefix prepended. + + :param vocab_words: list - of vocabulary words with prefix in first index. + :return: str - of prefix followed by vocabulary words with + prefix applied. + + This function takes a `vocab_words` list and returns a string + with the prefix and the words with prefix applied, separated + by ' :: '. + + For example: list('en', 'close', 'joy', 'lighten'), + produces the following string: 'en :: enclose :: enjoy :: enlighten'. + """ + return f' :: {vocab_words[0]}'.join(vocab_words) + + +def remove_suffix_ness(word: str) -> str: + """ + Remove the suffix from the word while keeping spelling in mind. + + :param word: str - of word to remove suffix from. + :return: str - of word with suffix removed & spelling adjusted. + + For example: "heaviness" becomes "heavy", but "sadness" becomes "sad". + """ + return f'{word[:-5]}y' if word[-5] == 'i' else word[:-4] + + +def adjective_to_verb(sentence: str, index: int): + """ + Change the adjective within the sentence to a verb. + + :param sentence: str - that uses the word in sentence. + :param index: int - index of the word to remove and transform. + :return: str - word that changes the extracted adjective to a verb. + + For example, ("It got dark as the sun set.", 2) becomes "darken". + """ + words: list[str] = sentence.split() + word: str = words[index].strip('.,!?') + return f'{word}en' diff --git a/solutions/python/making-the-grade/1/loops.py b/solutions/python/making-the-grade/1/loops.py new file mode 100644 index 0000000..2c922a6 --- /dev/null +++ b/solutions/python/making-the-grade/1/loops.py @@ -0,0 +1,84 @@ +"""Functions for organizing and calculating student exam scores.""" + + +def round_scores(student_scores: list) -> list: + """ + Round all provided student scores. + + :param student_scores: list - float or int of student exam scores. + :return: list - student scores *rounded* to nearest integer value. + """ + return [round(score) for score in student_scores] + + +def count_failed_students(student_scores: list) -> int: + """ + Count the number of failing students out of the group provided. + + :param student_scores: list - containing int student scores. + :return: int - count of student scores at or below 40. + """ + return len([score for score in student_scores if score <= 40.0]) + + +def above_threshold(student_scores: list, + threshold: int) -> list: + """ + Filter out above threshold scores. + + Determine how many of the provided student scores were 'the best' + based on the provided threshold. + + :param student_scores: list - of integer scores. + :param threshold: int - threshold to cross to be the "best" score. + :return: list - of integer scores that are at or above the "best" threshold. + """ + return [score for score in student_scores if score >= threshold] + + +def letter_grades(highest: int) -> list: + """ + Create a list of grade thresholds based on the provided highest grade. + + :param highest: int - value of highest exam score. + :return: list - of lower threshold scores for each D-A letter grade interval. + For example, where the highest score is 100, and failing is <= 40, + The result would be [41, 56, 71, 86]: + + 41 <= "D" <= 55 + 56 <= "C" <= 70 + 71 <= "B" <= 85 + 86 <= "A" <= 100 + """ + interval = (highest - 40) // 4 + return [41 + i * interval for i in range(4)] + + +def student_ranking(student_scores: list, + student_names: list) -> list[str]: + """ + Organize the student's rank, name, and grade information in descending order. + + :param student_scores: list - of scores in descending order. + :param student_names: list - of string names by exam score in descending order. + :return: list - of strings in format [". : "]. + """ + return [f"{i}. {name}: {score}" for i, score, name in zip( + range(1, len(student_scores) + 1), student_scores, student_names)] + + +def perfect_score(student_info: list) -> list: + """ + Create a list that contains the name and grade of the first + student to make a perfect score on the exam. + + :param student_info: list - of [, ] lists. + :return: list - first `[, 100]` or `[]` if no + student score of 100 is found. + """ + result_lst: list = [] + for student, score in student_info: + if score == 100: + return [student, score] + + return result_lst diff --git a/solutions/python/making-the-grade/2/loops.py b/solutions/python/making-the-grade/2/loops.py new file mode 100644 index 0000000..afe3994 --- /dev/null +++ b/solutions/python/making-the-grade/2/loops.py @@ -0,0 +1,83 @@ +"""Functions for organizing and calculating student exam scores.""" + + +def round_scores(student_scores: list) -> list: + """ + Round all provided student scores. + + :param student_scores: list - float or int of student exam scores. + :return: list - student scores *rounded* to nearest integer value. + """ + return [round(score) for score in student_scores] + + +def count_failed_students(student_scores: list) -> int: + """ + Count the number of failing students out of the group provided. + + :param student_scores: list - containing int student scores. + :return: int - count of student scores at or below 40. + """ + return len([score for score in student_scores if score <= 40.0]) + + +def above_threshold(student_scores: list, + threshold: int) -> list: + """ + Filter out above threshold scores. + + Determine how many of the provided student scores were 'the best' + based on the provided threshold. + + :param student_scores: list - of integer scores. + :param threshold: int - threshold to cross to be the "best" score. + :return: list - of integer scores that are at or above the "best" threshold. + """ + return [score for score in student_scores if score >= threshold] + + +def letter_grades(highest: int) -> list: + """ + Create a list of grade thresholds based on the provided highest grade. + + :param highest: int - value of highest exam score. + :return: list - of lower threshold scores for each D-A letter grade interval. + For example, where the highest score is 100, and failing is <= 40, + The result would be [41, 56, 71, 86]: + + 41 <= "D" <= 55 + 56 <= "C" <= 70 + 71 <= "B" <= 85 + 86 <= "A" <= 100 + """ + interval = (highest - 40) // 4 + return [41 + i * interval for i in range(4)] + + +def student_ranking(student_scores: list, + student_names: list) -> list[str]: + """ + Organize the student's rank, name, and grade information in descending order. + + :param student_scores: list - of scores in descending order. + :param student_names: list - of string names by exam score in descending order. + :return: list - of strings in format [". : "]. + """ + return [f"{i}. {name}: {score}" for i, score, name in zip( + range(1, len(student_scores) + 1), student_scores, student_names)] + + +def perfect_score(student_info: list) -> list: + """ + Create a list that contains the name and grade of the first + student to make a perfect score on the exam. + + :param student_info: list - of [, ] lists. + :return: list - first `[, 100]` or `[]` if no + student score of 100 is found. + """ + for student, score in student_info: + if score == 100: + return [student, score] + + return [] diff --git a/solutions/python/meltdown-mitigation/1/conditionals.py b/solutions/python/meltdown-mitigation/1/conditionals.py new file mode 100644 index 0000000..87b831b --- /dev/null +++ b/solutions/python/meltdown-mitigation/1/conditionals.py @@ -0,0 +1,71 @@ +"""Functions to prevent a nuclear meltdown.""" + + +def is_criticality_balanced(temperature, neutrons_emitted) -> bool: + """Verify criticality is balanced. + + :param temperature: int or float - temperature value in kelvin. + :param neutrons_emitted: int or float - number of neutrons emitted per second. + :return: bool - is criticality balanced? + + A reactor is said to be critical if it satisfies the following conditions: + - The temperature is less than 800 K. + - The number of neutrons emitted per second is greater than 500. + - The product of temperature and neutrons emitted per second is less than 500000. + """ + return temperature < 800 and neutrons_emitted > 500 and (temperature * neutrons_emitted) < 500000 + + +def reactor_efficiency(voltage, current, theoretical_max_power) -> str: + """Assess reactor efficiency zone. + + :param voltage: int or float - voltage value. + :param current: int or float - current value. + :param theoretical_max_power: int or float - power that corresponds to a 100% efficiency. + :return: str - one of ('green', 'orange', 'red', or 'black'). + + Efficiency can be grouped into 4 bands: + + 1. green -> efficiency of 80% or more, + 2. orange -> efficiency of less than 80% but at least 60%, + 3. red -> efficiency below 60%, but still 30% or more, + 4. black -> less than 30% efficient. + + The percentage value is calculated as + (generated power/ theoretical max power)*100 + where generated power = voltage * current + """ + generated_power = voltage * current + efficiency = (generated_power/theoretical_max_power)*100 + + if efficiency < 30: + return 'black' + elif 30 <= efficiency < 60: + return 'red' + elif 60 <= efficiency < 80: + return 'orange' + + return 'green' + + +def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str: + """Assess and return status code for the reactor. + + :param temperature: int or float - value of the temperature in kelvin. + :param neutrons_produced_per_second: int or float - neutron flux. + :param threshold: int or float - threshold for category. + :return: str - one of ('LOW', 'NORMAL', 'DANGER'). + + 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` + 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` + 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges + """ + current_state = (temperature * neutrons_produced_per_second) / 100 + thr_percent = threshold / 100 + + if thr_percent - 10 <= current_state <= thr_percent + 10: + return 'NORMAL' + elif current_state < thr_percent - 10: + return 'LOW' + + return 'DANGER' diff --git a/solutions/python/meltdown-mitigation/2/conditionals.py b/solutions/python/meltdown-mitigation/2/conditionals.py new file mode 100644 index 0000000..32fc5f5 --- /dev/null +++ b/solutions/python/meltdown-mitigation/2/conditionals.py @@ -0,0 +1,78 @@ +"""Functions to prevent a nuclear meltdown.""" + + +# pylint: disable=C0301 +def is_criticality_balanced(temperature, neutrons_emitted) -> bool: + """ + Verify criticality is balanced. + + :param temperature: int or float - temperature value in kelvin. + :param neutrons_emitted: int or float - number of neutrons emitted per second. + :return: bool - is criticality balanced? + + A reactor is said to be critical if it satisfies the following conditions: + - The temperature is less than 800 K. + - The number of neutrons emitted per second is greater than 500. + - The product of temperature and neutrons emitted per second is less than 500000. + """ + return temperature < 800 and neutrons_emitted > 500 and (temperature * neutrons_emitted) < 500000 + + +def reactor_efficiency(voltage, current, theoretical_max_power) -> str: + """ + Assess reactor efficiency zone. + + :param voltage: int or float - voltage value. + :param current: int or float - current value. + :param theoretical_max_power: int or float - power that corresponds to a 100% efficiency. + :return: str - one of ('green', 'orange', 'red', or 'black'). + + Efficiency can be grouped into 4 bands: + + 1. green -> efficiency of 80% or more, + 2. orange -> efficiency of less than 80% but at least 60%, + 3. red -> efficiency below 60%, but still 30% or more, + 4. black -> less than 30% efficient. + + The percentage value is calculated as + (generated power/ theoretical max power)*100 + where generated power = voltage * current + """ + generated_power = voltage * current + efficiency = (generated_power/theoretical_max_power)*100 + + if efficiency < 30: + return 'black' + + if 30 <= efficiency < 60: + return 'red' + + if 60 <= efficiency < 80: + return 'orange' + + return 'green' + + +def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str: + """ + Assess and return status code for the reactor. + + :param temperature: int or float - value of the temperature in kelvin. + :param neutrons_produced_per_second: int or float - neutron flux. + :param threshold: int or float - threshold for category. + :return: str - one of ('LOW', 'NORMAL', 'DANGER'). + + 1. 'LOW' -> `temperature * neutrons per second` < 90% of `threshold` + 2. 'NORMAL' -> `temperature * neutrons per second` +/- 10% of `threshold` + 3. 'DANGER' -> `temperature * neutrons per second` is not in the above-stated ranges + """ + current_state = (temperature * neutrons_produced_per_second) / 100 + thr_percent = threshold / 100 + + if thr_percent - 10 <= current_state <= thr_percent + 10: + return 'NORMAL' + + if current_state < thr_percent - 10: + return 'LOW' + + return 'DANGER' diff --git a/solutions/python/square-root/1/square_root.py b/solutions/python/square-root/1/square_root.py new file mode 100644 index 0000000..d830970 --- /dev/null +++ b/solutions/python/square-root/1/square_root.py @@ -0,0 +1,16 @@ +"""Solution for Square Root.""" + +from math import sqrt + + +def square_root(number: float) -> int: + """ + Calculate square root. + + :param number: any number + :type number: float + :return: square root of any number, + only natural numbers (positive integers) as solutions. + :rtype: int + """ + return int(sqrt(number)) diff --git a/solutions/python/triangle/1/triangle.py b/solutions/python/triangle/1/triangle.py new file mode 100644 index 0000000..4d60e40 --- /dev/null +++ b/solutions/python/triangle/1/triangle.py @@ -0,0 +1,49 @@ +"""Determine if a triangle is equilateral, isosceles, or scalene.""" + + +def equilateral(sides: list) -> bool: + """ + An equilateral triangle has all three sides the same length. + """ + if check_for_zeroes(sides) and inequality_violation(sides): + return sides[0] == sides[1] == sides[2] + return False + + +def isosceles(sides: list) -> bool: + """ + An isosceles triangle has at least two sides the same length. + (It is sometimes specified as having exactly two sides the same length, + but for the purposes of this exercise we'll say at least two.) + """ + if check_for_zeroes(sides) and inequality_violation(sides): + return sides[0] == sides[1] or sides[1] == sides[2] or sides[0] == sides[2] + return False + + +def scalene(sides: list) -> bool: + """ + A scalene triangle has all sides of different lengths. + """ + if check_for_zeroes(sides) and inequality_violation(sides): + return sides[0] != sides[1] and sides[1] != sides[2] and sides[0] != sides[2] + return False + + +def inequality_violation(sides: list) -> bool: + """ + Let a, b, and c be sides of the triangle. + Then all three of the following expressions must be true: + + a + b ≥ c + b + c ≥ a + a + c ≥ b + """ + return sides[0] + sides[1] >= sides[2] and sides[0] + sides[2] >= sides[1] and sides[2] + sides[1] >= sides[0] + + +def check_for_zeroes(sides: list) -> bool: + """No zeroes allowed.""" + if 0 in sides: + return False + return True diff --git a/solutions/python/triangle/2/triangle.py b/solutions/python/triangle/2/triangle.py new file mode 100644 index 0000000..bac8e96 --- /dev/null +++ b/solutions/python/triangle/2/triangle.py @@ -0,0 +1,47 @@ +"""Determine if a triangle is equilateral, isosceles, or scalene.""" + + +def equilateral(sides: list) -> bool: + """ + An equilateral triangle has all three sides the same length. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] == sides[2] + return False + + +def isosceles(sides: list) -> bool: + """ + An isosceles triangle has at least two sides the same length. + (It is sometimes specified as having exactly two sides the same length, + but for the purposes of this exercise we'll say at least two.) + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] or sides[1] == sides[2] or sides[0] == sides[2] + return False + + +def scalene(sides: list) -> bool: + """ + A scalene triangle has all sides of different lengths. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] != sides[1] and sides[1] != sides[2] and sides[0] != sides[2] + return False + + +def no_inequality_violation(sides: list) -> bool: + """ + Let a, b, and c be sides of the triangle. + Then all three of the following expressions must be true: + + a + b ≥ c + b + c ≥ a + a + c ≥ b + """ + return sides[0] + sides[1] >= sides[2] and sides[0] + sides[2] >= sides[1] and sides[2] + sides[1] >= sides[0] + + +def all_sides_positive(sides: list) -> bool: + """No zeroes or negative numbers allowed.""" + return all(side > 0 for side in sides) and len(sides) == 3 diff --git a/solutions/python/triangle/3/triangle.py b/solutions/python/triangle/3/triangle.py new file mode 100644 index 0000000..6537e75 --- /dev/null +++ b/solutions/python/triangle/3/triangle.py @@ -0,0 +1,52 @@ +"""Determine if a triangle is equilateral, isosceles, or scalene.""" + + +def equilateral(sides: list) -> bool: + """ + An equilateral triangle has all three sides the same length. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] == sides[2] + return False + + +def isosceles(sides: list) -> bool: + """ + An isosceles triangle has at least two sides the same length. + (It is sometimes specified as having exactly two sides the same length, + but for the purposes of this exercise we'll say at least two.) + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] or sides[1] == sides[2] or sides[0] == sides[2] + return False + + +def scalene(sides: list) -> bool: + """ + A scalene triangle has all sides of different lengths. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] != sides[1] and sides[1] != sides[2] and sides[0] != sides[2] + return False + + +def no_inequality_violation(sides: list) -> bool: + """ + Let a, b, and c be sides of the triangle. + Then all three of the following expressions must be true: + + a + b ≥ c + b + c ≥ a + a + c ≥ b + """ + return (sides[0] + sides[1] >= sides[2] and + sides[0] + sides[2] >= sides[1] and + sides[2] + sides[1] >= sides[0]) + + +def all_sides_positive(sides: list) -> bool: + """ + No zeroes or negative numbers allowed. + All triangles should have 3 sides. + """ + return all(side > 0 for side in sides) and len(sides) == 3 diff --git a/square-root/.exercism/config.json b/square-root/.exercism/config.json new file mode 100644 index 0000000..b1b9630 --- /dev/null +++ b/square-root/.exercism/config.json @@ -0,0 +1,21 @@ +{ + "authors": [ + "meatball133", + "Bethanyg" + ], + "contributors": [], + "files": { + "solution": [ + "square_root.py" + ], + "test": [ + "square_root_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Given a natural radicand, return its square root.", + "source": "wolf99", + "source_url": "https://github.com/exercism/problem-specifications/pull/1582" +} diff --git a/square-root/.exercism/metadata.json b/square-root/.exercism/metadata.json new file mode 100644 index 0000000..b4be30a --- /dev/null +++ b/square-root/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"square-root","id":"15f9c335485f4af59694fd86171e39d9","url":"https://exercism.org/tracks/python/exercises/square-root","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/square-root/HELP.md b/square-root/HELP.md new file mode 100644 index 0000000..1659db8 --- /dev/null +++ b/square-root/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 square_root.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/square-root/README.md b/square-root/README.md new file mode 100644 index 0000000..f65bb95 --- /dev/null +++ b/square-root/README.md @@ -0,0 +1,60 @@ +# Square Root + +Welcome to Square Root on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Introduction + +We are launching a deep space exploration rocket and we need a way to make sure the navigation system stays on target. + +As the first step in our calculation, we take a target number and find its square root (that is, the number that when multiplied by itself equals the target number). + +The journey will be very long. +To make the batteries last as long as possible, we had to make our rocket's onboard computer very power efficient. +Unfortunately that means that we can't rely on fancy math libraries and functions, as they use more power. +Instead we want to implement our own square root calculation. + +## Instructions + +Your task is to calculate the square root of a given number. + +- Try to avoid using the pre-existing math libraries of your language. +- As input you'll be given a positive whole number, i.e. 1, 2, 3, 4… +- You are only required to handle cases where the result is a positive whole number. + +Some potential approaches: + +- Linear or binary search for a number that gives the input number when squared. +- Successive approximation using Newton's or Heron's method. +- Calculating one digit at a time or one bit at a time. + +You can check out the Wikipedia pages on [integer square root][integer-square-root] and [methods of computing square roots][computing-square-roots] to help with choosing a method of calculation. + +[integer-square-root]: https://en.wikipedia.org/wiki/Integer_square_root +[computing-square-roots]: https://en.wikipedia.org/wiki/Methods_of_computing_square_roots + +## How this Exercise is Structured in Python + + +Python offers a wealth of mathematical functions in the form of the [math module][math-module] and built-ins such as the exponentiation operator `**`, [`pow()`][pow] and [`sum()`][sum]. +However, we'd like you to consider the challenge of solving this exercise without those built-ins or modules. + +While there is a mathematical formula that will find the square root of _any_ number, we have gone the route of having only [natural numbers][nautral-number] (positive integers) as solutions. +It is possible to compute the square root of any natural number using only natural numbers in the computation. + + +[math-module]: https://docs.python.org/3/library/math.html +[pow]: https://docs.python.org/3/library/functions.html#pow +[sum]: https://docs.python.org/3/library/functions.html#sum +[nautral-number]: https://en.wikipedia.org/wiki/Natural_number + +## Source + +### Created by + +- @meatball133 +- @Bethanyg + +### Based on + +wolf99 - https://github.com/exercism/problem-specifications/pull/1582 \ No newline at end of file diff --git a/square-root/square_root.py b/square-root/square_root.py new file mode 100644 index 0000000..d830970 --- /dev/null +++ b/square-root/square_root.py @@ -0,0 +1,16 @@ +"""Solution for Square Root.""" + +from math import sqrt + + +def square_root(number: float) -> int: + """ + Calculate square root. + + :param number: any number + :type number: float + :return: square root of any number, + only natural numbers (positive integers) as solutions. + :rtype: int + """ + return int(sqrt(number)) diff --git a/square-root/square_root_test.py b/square-root/square_root_test.py new file mode 100644 index 0000000..fcd2bb7 --- /dev/null +++ b/square-root/square_root_test.py @@ -0,0 +1,41 @@ +# pylint: disable=C0301 +""" +Unit tests for the square_root function, verifying +correct calculation of integer square roots for various +inputs using test data from Exercism. +""" + +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/square-root/canonical-data.json +# File last updated on 2023-07-19 + +import unittest + +from square_root import ( + square_root, +) + + +# pylint: disable=C0116 +class SquareRootTest(unittest.TestCase): + """ + Test suite for verifying the functionality of the square_root function. + """ + + def test_root_of_1(self): + self.assertEqual(square_root(1), 1) + + def test_root_of_4(self): + self.assertEqual(square_root(4), 2) + + def test_root_of_25(self): + self.assertEqual(square_root(25), 5) + + def test_root_of_81(self): + self.assertEqual(square_root(81), 9) + + def test_root_of_196(self): + self.assertEqual(square_root(196), 14) + + def test_root_of_65025(self): + self.assertEqual(square_root(65025), 255) diff --git a/triangle/.exercism/config.json b/triangle/.exercism/config.json new file mode 100644 index 0000000..041bf28 --- /dev/null +++ b/triangle/.exercism/config.json @@ -0,0 +1,36 @@ +{ + "authors": [], + "contributors": [ + "behrtam", + "BethanyG", + "cmccandless", + "Dog", + "ikhadykin", + "kytrinyx", + "lowks", + "mpatibandla", + "N-Parsons", + "Nishant23", + "pheanex", + "rozuur", + "sjakobi", + "Stigjb", + "tqa236", + "xitanggg", + "YuriyCherniy" + ], + "files": { + "solution": [ + "triangle.py" + ], + "test": [ + "triangle_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Determine if a triangle is equilateral, isosceles, or scalene.", + "source": "The Ruby Koans triangle project, parts 1 & 2", + "source_url": "https://web.archive.org/web/20220831105330/http://rubykoans.com" +} diff --git a/triangle/.exercism/metadata.json b/triangle/.exercism/metadata.json new file mode 100644 index 0000000..0b73088 --- /dev/null +++ b/triangle/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"triangle","id":"6f8c14ada1ce403c975161c5a18528c2","url":"https://exercism.org/tracks/python/exercises/triangle","handle":"myFirstCode","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/triangle/HELP.md b/triangle/HELP.md new file mode 100644 index 0000000..2d9233f --- /dev/null +++ b/triangle/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 triangle.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/triangle/README.md b/triangle/README.md new file mode 100644 index 0000000..961c673 --- /dev/null +++ b/triangle/README.md @@ -0,0 +1,60 @@ +# Triangle + +Welcome to Triangle on Exercism's Python Track. +If you need help running the tests or submitting your code, check out `HELP.md`. + +## Instructions + +Determine if a triangle is equilateral, isosceles, or scalene. + +An _equilateral_ triangle has all three sides the same length. + +An _isosceles_ triangle has at least two sides the same length. +(It is sometimes specified as having exactly two sides the same length, but for the purposes of this exercise we'll say at least two.) + +A _scalene_ triangle has all sides of different lengths. + +## Note + +For a shape to be a triangle at all, all sides have to be of length > 0, and the sum of the lengths of any two sides must be greater than or equal to the length of the third side. + +In equations: + +Let `a`, `b`, and `c` be sides of the triangle. +Then all three of the following expressions must be true: + +```text +a + b ≥ c +b + c ≥ a +a + c ≥ b +``` + +See [Triangle Inequality][triangle-inequality] + +[triangle-inequality]: https://en.wikipedia.org/wiki/Triangle_inequality + +## Source + +### Contributed to by + +- @behrtam +- @BethanyG +- @cmccandless +- @Dog +- @ikhadykin +- @kytrinyx +- @lowks +- @mpatibandla +- @N-Parsons +- @Nishant23 +- @pheanex +- @rozuur +- @sjakobi +- @Stigjb +- @tqa236 +- @xitanggg +- @YuriyCherniy + +### Based on + +The Ruby Koans triangle project, parts 1 & 2 - https://web.archive.org/web/20220831105330/http://rubykoans.com \ No newline at end of file diff --git a/triangle/triangle.py b/triangle/triangle.py new file mode 100644 index 0000000..eefee03 --- /dev/null +++ b/triangle/triangle.py @@ -0,0 +1,54 @@ +"""Determine if a triangle is equilateral, isosceles, or scalene.""" + + +def equilateral(sides: list) -> bool: + """ + An equilateral triangle has all three sides the same length. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] == sides[2] + return False + + +def isosceles(sides: list) -> bool: + """ + An isosceles triangle has at least two sides the same length. + (It is sometimes specified as having exactly two sides the same length, + but for the purposes of this exercise we'll say at least two.) + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] == sides[1] or sides[1] == sides[2] or sides[0] == sides[2] + return False + + +def scalene(sides: list) -> bool: + """ + A scalene triangle has all sides of different lengths. + """ + if all_sides_positive(sides) and no_inequality_violation(sides): + return sides[0] != sides[1] and sides[1] != sides[2] and sides[0] != sides[2] + return False + + +def no_inequality_violation(sides: list) -> bool: + """ + Let a, b, and c be sides of the triangle. + Then all three of the following expressions must be true: + + a + b ≥ c + b + c ≥ a + a + c ≥ b + """ + return ( + sides[0] + sides[1] >= sides[2] + and sides[0] + sides[2] >= sides[1] + and sides[2] + sides[1] >= sides[0] + ) + + +def all_sides_positive(sides: list) -> bool: + """ + No zeroes or negative numbers allowed. + All triangles should have 3 sides. + """ + return all(side > 0 for side in sides) and len(sides) == 3 diff --git a/triangle/triangle_test.py b/triangle/triangle_test.py new file mode 100644 index 0000000..9312b90 --- /dev/null +++ b/triangle/triangle_test.py @@ -0,0 +1,85 @@ +# pylint: disable=C0116, C0114, C0115 + +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/triangle/canonical-data.json +# File last updated on 2023-07-19 + +import unittest + +from triangle import ( + equilateral, + isosceles, + scalene, +) + + +# pylint: disable=C0116, C0114, C0115 +class EquilateralTriangleTest(unittest.TestCase): + def test_all_sides_are_equal(self): + self.assertIs(equilateral([2, 2, 2]), True) + + def test_any_side_is_unequal(self): + self.assertIs(equilateral([2, 3, 2]), False) + + def test_no_sides_are_equal(self): + self.assertIs(equilateral([5, 4, 6]), False) + + def test_all_zero_sides_is_not_a_triangle(self): + self.assertIs(equilateral([0, 0, 0]), False) + + def test_sides_may_be_floats(self): + self.assertIs(equilateral([0.5, 0.5, 0.5]), True) + + +# pylint: disable=C0116 +class IsoscelesTriangleTest(unittest.TestCase): + def test_last_two_sides_are_equal(self): + self.assertIs(isosceles([3, 4, 4]), True) + + def test_first_two_sides_are_equal(self): + self.assertIs(isosceles([4, 4, 3]), True) + + def test_first_and_last_sides_are_equal(self): + self.assertIs(isosceles([4, 3, 4]), True) + + def test_equilateral_triangles_are_also_isosceles(self): + self.assertIs(isosceles([4, 4, 4]), True) + + def test_no_sides_are_equal(self): + self.assertIs(isosceles([2, 3, 4]), False) + + def test_first_triangle_inequality_violation(self): + self.assertIs(isosceles([1, 1, 3]), False) + + def test_second_triangle_inequality_violation(self): + self.assertIs(isosceles([1, 3, 1]), False) + + def test_third_triangle_inequality_violation(self): + self.assertIs(isosceles([3, 1, 1]), False) + + def test_sides_may_be_floats(self): + self.assertIs(isosceles([0.5, 0.4, 0.5]), True) + + +# pylint: disable=C0116 +class ScaleneTriangleTest(unittest.TestCase): + def test_no_sides_are_equal(self): + self.assertIs(scalene([5, 4, 6]), True) + + def test_all_sides_are_equal(self): + self.assertIs(scalene([4, 4, 4]), False) + + def test_first_and_second_sides_are_equal(self): + self.assertIs(scalene([4, 4, 3]), False) + + def test_first_and_third_sides_are_equal(self): + self.assertIs(scalene([3, 4, 3]), False) + + def test_second_and_third_sides_are_equal(self): + self.assertIs(scalene([4, 3, 3]), False) + + def test_may_not_violate_triangle_inequality(self): + self.assertIs(scalene([7, 3, 2]), False) + + def test_sides_may_be_floats(self): + self.assertIs(scalene([0.5, 0.4, 0.6]), True)