├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── src ├── .DS_Store └── katas │ ├── .DS_Store │ ├── __init__.py │ ├── alphabet_position.py │ ├── array_diff.py │ ├── chain_adding.py │ ├── env_secret_generator.py │ ├── exceptions.py │ ├── fizz_buzz.py │ ├── gilded_rose │ ├── __init__.py │ ├── gilded_rose.py │ └── item_factory.py │ ├── prime_factors.py │ ├── read_write_json.py │ ├── roman_numerals.py │ ├── string_calculator.py │ ├── tennis │ ├── __init__.py │ └── tennis_match.py │ └── unique_string_finder.py └── tests ├── .DS_Store ├── __init__.py ├── alphabet_position_test.py ├── array_diff_test.py ├── chain_adding_test.py ├── env_secret_generator_test.py ├── fixtures ├── content.txt └── data_file.json ├── fizz_buzz_test.py ├── gilded_rose_test.py ├── prime_factors_test.py ├── read_write_json_test.py ├── roman_numerals_test.py ├── string_calculator_test.py ├── tennis_match_test.py └── unique_string_finder_test.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install flake8 pytest 23 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 24 | - name: Lint with flake8 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with python unittest 31 | run: | 32 | python -m unittest discover tests -p '*_test.py' 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | src/__pycache__/ 9 | src/katas/__pycache__/ 10 | tests/__pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | venv 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | 143 | # End of https://www.toptal.com/developers/gitignore/api/python 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Thavarshan Thayananthajothy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code Katas with Python Unittest 2 | 3 | ## Introduction 4 | 5 | If martial artists use kata as a method for exercise and practice, what might be the equivalent for coders like us? Coding katas are short, repeatable programming challenges which are meant to exercise everything from your focus, to your workflow. 6 | 7 | This was already done in PHPUnit which you can find [here](https://github.com/Thavarshan/phpunit-code-katas), so this time I'm doing it in Python. 8 | 9 | ## Katas 10 | 11 | - Prime Factors 12 | - Roman Numerals 13 | - Bowling Game 14 | - String Calculator 15 | - Tennis Match 16 | - FizzBuzz 17 | - The Gilded Rose 18 | 19 | ## Installation 20 | 21 | ### Prerequisites 22 | 23 | To run this project, you must have Python 3.6 or higher installed. 24 | 25 | Begin by cloning this repository to your machine, and installing all dependencies dependencies. 26 | 27 | ### Get Started 28 | 29 | ```bash 30 | git clone git@github.com:Thavarshan/python-code-katas.git katas 31 | cd katas && pip install -r requirements.txt 32 | ``` 33 | 34 | ## Testing 35 | 36 | Just run Python unit test in the project root. 37 | 38 | ```bash 39 | cd katas 40 | python -m venv ./venv 41 | source ./venv/bin/activate 42 | python -m unittest discover tests -p '*_test.py' 43 | ``` 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | unittest-data-provider==1.0.1 2 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/src/.DS_Store -------------------------------------------------------------------------------- /src/katas/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/src/katas/.DS_Store -------------------------------------------------------------------------------- /src/katas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/src/katas/__init__.py -------------------------------------------------------------------------------- /src/katas/alphabet_position.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class AlphabetPosition: 5 | 6 | alphabet = { 7 | 'a': 1, 8 | 'b': 2, 9 | 'c': 3, 10 | 'd': 4, 11 | 'e': 5, 12 | 'f': 6, 13 | 'g': 7, 14 | 'h': 8, 15 | 'i': 9, 16 | 'j': 10, 17 | 'k': 11, 18 | 'l': 12, 19 | 'm': 13, 20 | 'n': 14, 21 | 'o': 15, 22 | 'p': 16, 23 | 'q': 17, 24 | 'r': 18, 25 | 's': 19, 26 | 't': 20, 27 | 'u': 21, 28 | 'v': 22, 29 | 'w': 23, 30 | 'x': 24, 31 | 'y': 25, 32 | 'z': 26, 33 | } 34 | 35 | def find_position(self, sentence: str): 36 | # Convert all letters to lowercase 37 | sentence = sentence.lower() 38 | # Remove all spaces and split sentence to list of chars 39 | sentence = sentence.replace(" ", "") 40 | # Extract only letters 41 | characters = ''.join(re.findall("[a-zA-Z]+", sentence)) 42 | # Make string into list of characters 43 | characters = list(characters) 44 | # Initiate an empty list to save all positions of the characters in 45 | positions = [] 46 | # Iterate through each character and find its position in the alphabet. 47 | # once found replace the character with it's relevant position number 48 | for character in characters: 49 | positions.append(self.alphabet.get(character)) 50 | # Convert list of integers to single string 51 | return ' '.join(map(str, positions)) 52 | -------------------------------------------------------------------------------- /src/katas/array_diff.py: -------------------------------------------------------------------------------- 1 | class ArrayDiff: 2 | 3 | def differentiate(self, arrayOne: list, arrayTwo: list) -> list: 4 | return [item for item in arrayOne if item not in set(arrayTwo)] 5 | -------------------------------------------------------------------------------- /src/katas/chain_adding.py: -------------------------------------------------------------------------------- 1 | class Add(int): 2 | 3 | def __call__(self, number): 4 | return Add(self + number) 5 | -------------------------------------------------------------------------------- /src/katas/env_secret_generator.py: -------------------------------------------------------------------------------- 1 | import re 2 | import secrets 3 | 4 | 5 | class SecretKeyGenerator: 6 | 7 | env_variable_key = 'SECRET_KEY' 8 | file = '.env' 9 | 10 | def __init__(self, file=None): 11 | self.file = file if file is not None else self.file 12 | 13 | def write_to_file(self): 14 | if not self.check_str_in_lines(self.env_variable_key): 15 | with open(self.file, 'a+') as env_file: 16 | env_file.write(self.make_key_and_value()) 17 | 18 | def generate_skey(self): 19 | return secrets.token_urlsafe(16) 20 | 21 | def make_key_and_value(self): 22 | line = self.env_variable_key + '=' + self.generate_skey() 23 | return line.rstrip('\r\n') + '\n' 24 | 25 | def check_str_in_lines(self, check): 26 | with open(self.file, 'r+') as file: 27 | for line in file.readlines(): 28 | line = re.sub(r'[\n\t\s]*', '', line) 29 | if check in line: 30 | return True 31 | else: 32 | return False 33 | -------------------------------------------------------------------------------- /src/katas/exceptions.py: -------------------------------------------------------------------------------- 1 | class IllegalArgumentError(ValueError): 2 | pass 3 | -------------------------------------------------------------------------------- /src/katas/fizz_buzz.py: -------------------------------------------------------------------------------- 1 | class FizzBuzz: 2 | 3 | def convert(self, number: int): 4 | result = '' 5 | 6 | if number % 3 == 0: 7 | result += 'Fizz' 8 | 9 | if number % 5 == 0: 10 | result += 'Buzz' 11 | 12 | if result != '': 13 | return result 14 | else: 15 | return number 16 | -------------------------------------------------------------------------------- /src/katas/gilded_rose/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/src/katas/gilded_rose/__init__.py -------------------------------------------------------------------------------- /src/katas/gilded_rose/gilded_rose.py: -------------------------------------------------------------------------------- 1 | from ..exceptions import IllegalArgumentError 2 | import importlib 3 | 4 | 5 | class GildedRose: 6 | 7 | items = { 8 | 'normal': 'Normal', 9 | 'Aged Brie': 'Brie', 10 | 'Sulfuras, Hand of Ragnaros': 'Sulfuras', 11 | 'Backstage passes to a TAFKAL80ETC concert': 'BackstagePasses', 12 | 'Conjured Juna Cake': 'Conjured', 13 | } 14 | 15 | def of(self, name, quality, sell_in): 16 | if name in self.items: 17 | module = importlib.import_module( 18 | 'src.katas.gilded_rose.item_factory' 19 | ) 20 | class_ = getattr(module, self.items[name]) 21 | 22 | return class_(quality, sell_in) 23 | 24 | raise IllegalArgumentError('Item type does not exist.') 25 | -------------------------------------------------------------------------------- /src/katas/gilded_rose/item_factory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class AbstractItem(ABC): 5 | quality = 0 6 | sell_in = 0 7 | 8 | def __init__(self, quality: int, sell_in: int): 9 | self.quality = quality 10 | self.sell_in = sell_in 11 | 12 | @abstractmethod 13 | def tick(self): 14 | pass 15 | 16 | 17 | class Normal(AbstractItem): 18 | 19 | def tick(self): 20 | self.sell_in -= 1 21 | self.quality -= 1 22 | 23 | if self.sell_in <= 0: 24 | self.quality -= 1 25 | 26 | if self.quality < 0: 27 | self.quality = 0 28 | 29 | 30 | class Brie(AbstractItem): 31 | 32 | def tick(self): 33 | self.sell_in -= 1 34 | self.quality += 1 35 | 36 | if self.sell_in <= 0: 37 | self.quality += 1 38 | 39 | if self.quality > 50: 40 | self.quality = 50 41 | 42 | 43 | class Sulfuras(AbstractItem): 44 | 45 | def tick(self): 46 | return True 47 | 48 | 49 | class BackstagePasses(AbstractItem): 50 | 51 | def tick(self): 52 | self.quality += 1 53 | 54 | if self.sell_in <= 10: 55 | self.quality += 1 56 | 57 | if self.sell_in <= 5: 58 | self.quality += 1 59 | 60 | if self.sell_in <= 0: 61 | self.quality = 0 62 | 63 | if self.quality > 50: 64 | self.quality = 50 65 | 66 | self.sell_in -= 1 67 | 68 | 69 | class Conjured(AbstractItem): 70 | 71 | def tick(self): 72 | self.sell_in -= 1 73 | self.quality -= 2 74 | 75 | if self.sell_in <= 0: 76 | self.quality -= 2 77 | 78 | if self.quality < 0: 79 | self.quality = 0 80 | -------------------------------------------------------------------------------- /src/katas/prime_factors.py: -------------------------------------------------------------------------------- 1 | class PrimeFactors(): 2 | 3 | def generate(self, number): 4 | factors = [] 5 | divisor = 2 6 | 7 | while number > 1: 8 | while number % divisor == 0: 9 | factors.append(divisor) 10 | 11 | number = number / divisor 12 | 13 | divisor += 1 14 | 15 | return factors 16 | -------------------------------------------------------------------------------- /src/katas/read_write_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def write_to_json_file(data: dict, file_name: str): 5 | with open(file_name, 'w+') as write_file: 6 | json.dump(data, write_file, indent=4) 7 | 8 | 9 | def read_from_json_file(file_name: str): 10 | with open(file_name, 'r+') as read_file: 11 | return json.load(read_file) 12 | -------------------------------------------------------------------------------- /src/katas/roman_numerals.py: -------------------------------------------------------------------------------- 1 | class RomanNumerals: 2 | 3 | numerals = { 4 | 'M': 1000, 5 | 'CM': 900, 6 | 'D': 500, 7 | 'CD': 400, 8 | 'C': 100, 9 | 'XC': 90, 10 | 'L': 50, 11 | 'XL': 40, 12 | 'X': 10, 13 | 'IX': 9, 14 | 'V': 5, 15 | 'IV': 4, 16 | 'I': 1, 17 | } 18 | 19 | def generate(self, number): 20 | if (number <= 0 or number >= 4000): 21 | return False 22 | 23 | result = '' 24 | 25 | for numeral, arabic in self.numerals.items(): 26 | while number >= arabic: 27 | result += numeral 28 | 29 | number -= arabic 30 | 31 | return result 32 | -------------------------------------------------------------------------------- /src/katas/string_calculator.py: -------------------------------------------------------------------------------- 1 | from .exceptions import IllegalArgumentError 2 | import re 3 | 4 | 5 | class StringCalculator: 6 | 7 | def add(self, numbers: str): 8 | if not numbers: 9 | return 0 10 | 11 | numbers = self.convert_to_integers(numbers) 12 | 13 | for number in numbers: 14 | self.is_negative_number(number) 15 | 16 | return sum(numbers) 17 | 18 | def convert_to_integers(self, string_list): 19 | numbers = list(map(int, re.split(',|\n', string_list))) 20 | 21 | return list(filter(lambda number: number <= 1000, numbers)) 22 | 23 | def is_negative_number(self, number: int): 24 | if number < 0: 25 | raise IllegalArgumentError('Numbers cannot be negative.') 26 | -------------------------------------------------------------------------------- /src/katas/tennis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/src/katas/tennis/__init__.py -------------------------------------------------------------------------------- /src/katas/tennis/tennis_match.py: -------------------------------------------------------------------------------- 1 | class Player: 2 | __points = 0 3 | 4 | def __init__(self, name: str): 5 | self._name = name 6 | 7 | def score(self): 8 | self.__points += 1 9 | 10 | def get_points(self): 11 | return self.__points 12 | 13 | def get_name(self): 14 | return self._name 15 | 16 | 17 | class Game: 18 | 19 | def __init__(self, player_one: Player, player_two: Player): 20 | self.player_one = player_one 21 | self.player_two = player_two 22 | 23 | def points_to(self, player_name: str): 24 | if self.player_one.get_name() == player_name: 25 | return self.player_one.score() 26 | 27 | if self.player_two.get_name() == player_name: 28 | return self.player_two.score() 29 | 30 | def score(self): 31 | if self._has_winner(): 32 | return f'Winner: {self._leader()}' 33 | 34 | if self._has_advantage(): 35 | return f'Advantage: {self._leader()}' 36 | 37 | if self._is_deuce(): 38 | return 'deuce' 39 | 40 | player_one_points = self._points_to_term(self.player_one.get_points()) 41 | player_two_points = self._points_to_term(self.player_two.get_points()) 42 | 43 | return f'{player_one_points}-{player_two_points}' 44 | 45 | def _leader(self): 46 | if self.player_one.get_points() > self.player_two.get_points(): 47 | return self.player_one.get_name() 48 | 49 | return self.player_two.get_name() 50 | 51 | def _has_winner(self): 52 | player_one_points = self.player_one.get_points() 53 | player_two_points = self.player_two.get_points() 54 | 55 | if player_one_points < 4 and player_two_points < 4: 56 | return False 57 | 58 | return abs(player_one_points - player_two_points) >= 2 59 | 60 | def _has_advantage(self): 61 | if not self._has_reached_deuce_threshold(): 62 | return False 63 | 64 | return not self._is_deuce() 65 | 66 | def _is_deuce(self): 67 | if not self._has_reached_deuce_threshold(): 68 | return False 69 | 70 | return self.player_one.get_points() == self.player_two.get_points() 71 | 72 | def _has_reached_deuce_threshold(self): 73 | player_one_points = self.player_one.get_points() 74 | player_two_points = self.player_two.get_points() 75 | 76 | return player_one_points >= 3 and player_two_points >= 3 77 | 78 | def _points_to_term(self, point: int): 79 | points = { 80 | 0: 'love', 81 | 1: 'fifteen', 82 | 2: 'thirty', 83 | 3: 'forty' 84 | } 85 | 86 | return points[point] 87 | -------------------------------------------------------------------------------- /src/katas/unique_string_finder.py: -------------------------------------------------------------------------------- 1 | class UniqueStringFinder: 2 | def find_unique(self, list_of_strings: list): 3 | list_of_strings.sort(key=lambda element: element.lower()) 4 | sorted_list = [set(element.lower()) for element in list_of_strings] 5 | 6 | if sorted_list.count(sorted_list[0]) == 1 and str(sorted_list[0]) != 'set()': 7 | return list_of_strings[0] 8 | else: 9 | return list_of_strings[-1] 10 | -------------------------------------------------------------------------------- /tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/tests/.DS_Store -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thavarshan/python-code-katas/b0e7c9c6d4808487d5e43e46313b266a3923564f/tests/__init__.py -------------------------------------------------------------------------------- /tests/alphabet_position_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import alphabet_position 2 | import unittest 3 | 4 | 5 | class AlphabetPositionTest(unittest.TestCase): 6 | 7 | def test_replaces_all_alphabetic_characters_to_its_relevant_position_in_the_alphabet(self): 8 | finder = alphabet_position.AlphabetPosition() 9 | 10 | self.assertEqual( 11 | "20 8 5 19 21 14 19 5 20 19 5 20 19 1 20 20 23 5 12 22 5 15 3 12 15 3 11", 12 | finder.find_position("The sunset sets at twelve o' clock.") 13 | ) 14 | -------------------------------------------------------------------------------- /tests/array_diff_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import array_diff 2 | import unittest 3 | 4 | 5 | class ArrayDiffTest(unittest.TestCase): 6 | 7 | def test_it_removes_all_values_from_first_list_which_is_present_in_second_list(self): 8 | differentiator = array_diff.ArrayDiff() 9 | 10 | self.assertEqual([1, 3], differentiator.differentiate([1, 2, 2, 2, 3], [2])) 11 | -------------------------------------------------------------------------------- /tests/chain_adding_test.py: -------------------------------------------------------------------------------- 1 | from src.katas.chain_adding import Add 2 | import unittest 3 | 4 | 5 | class ChainAddingTest(unittest.TestCase): 6 | 7 | def test_it_adds_numbers_together_when_called_in_succession(self): 8 | self.assertEqual(3, Add(1)(2)) 9 | self.assertEqual(6, Add(1)(2)(3)) 10 | self.assertEqual(10, Add(1)(2)(3)(4)) 11 | -------------------------------------------------------------------------------- /tests/env_secret_generator_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from src.katas.env_secret_generator import SecretKeyGenerator 4 | 5 | 6 | class TestSecretKeyGenerator(unittest.TestCase): 7 | 8 | def test_find_string_in_file(self): 9 | skey_generator = SecretKeyGenerator('tests/fixtures/content.txt') 10 | self.assertTrue(skey_generator.check_str_in_lines('Hello')) 11 | self.assertFalse(skey_generator.check_str_in_lines('hello')) 12 | 13 | def test_make_key_and_value(self): 14 | skey_generator = SecretKeyGenerator() 15 | self.assertTrue(('SECRET_KEY' in skey_generator.make_key_and_value())) 16 | 17 | def test_write_key_to_file(self): 18 | env_file = 'tests/fixtures/.env' 19 | self.create_dummy_file(env_file) 20 | 21 | skey_generator = SecretKeyGenerator(env_file) 22 | self.assertFalse(skey_generator.check_str_in_lines('SECRET_KEY')) 23 | skey_generator.write_to_file() 24 | self.assertTrue(skey_generator.check_str_in_lines('SECRET_KEY')) 25 | os.remove(env_file) 26 | 27 | def create_dummy_file(self, file_name): 28 | with open(file_name, 'w+') as file: 29 | pass 30 | 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /tests/fixtures/content.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | SECRET_KEY=-FqQ6eoFPm2P1A3ufx-6TA 3 | -------------------------------------------------------------------------------- /tests/fixtures/data_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "president": { 3 | "name": "Zaphod Beeblebrox", 4 | "species": "Betelgeusian" 5 | } 6 | } -------------------------------------------------------------------------------- /tests/fizz_buzz_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import fizz_buzz 2 | import unittest 3 | 4 | 5 | class FizzBuzzTest(unittest.TestCase): 6 | 7 | def test_it_return_fizz_for_multiples_of_three(self): 8 | converter = fizz_buzz.FizzBuzz() 9 | 10 | for number in [3, 6, 9, 12]: 11 | self.assertEqual('Fizz', converter.convert(number)) 12 | 13 | def test_it_return_buzz_for_multiples_of_five(self): 14 | converter = fizz_buzz.FizzBuzz() 15 | 16 | for number in [5, 10, 20, 25]: 17 | self.assertEqual('Buzz', converter.convert(number)) 18 | 19 | def test_it_return_fizzbuzz_for_multiples_of_three_and_five(self): 20 | converter = fizz_buzz.FizzBuzz() 21 | 22 | for number in [15, 30, 45, 60]: 23 | self.assertEqual('FizzBuzz', converter.convert(number)) 24 | 25 | def test_it_return_the_original_number_if_not_divisible_by_three_and_five(self): 26 | converter = fizz_buzz.FizzBuzz() 27 | 28 | for number in [1, 2, 4, 7]: 29 | self.assertEqual(number, converter.convert(number)) 30 | -------------------------------------------------------------------------------- /tests/gilded_rose_test.py: -------------------------------------------------------------------------------- 1 | from src.katas.gilded_rose import gilded_rose 2 | import unittest 3 | 4 | 5 | class GildedRoseTest(unittest.TestCase): 6 | 7 | def test_it_updates_normal_items_before_the_sell_date(self): 8 | shop = gilded_rose.GildedRose() 9 | item = shop.of('normal', 10, 5) 10 | item.tick() 11 | 12 | self.assertEqual(9, item.quality) 13 | self.assertEqual(4, item.sell_in) 14 | 15 | def test_it_updates_normal_items_on_the_sell_date(self): 16 | shop = gilded_rose.GildedRose() 17 | item = shop.of('normal', 10, 0) 18 | item.tick() 19 | 20 | self.assertEqual(8, item.quality) 21 | self.assertEqual(-1, item.sell_in) 22 | 23 | def test_it_updates_normal_items_after_the_sell_date(self): 24 | shop = gilded_rose.GildedRose() 25 | item = shop.of('normal', 10, -5) 26 | item.tick() 27 | 28 | self.assertEqual(8, item.quality) 29 | self.assertEqual(-6, item.sell_in) 30 | 31 | def test_it_updates_normal_items_with_a_quality_of_zero(self): 32 | shop = gilded_rose.GildedRose() 33 | item = shop.of('normal', 0, 5) 34 | item.tick() 35 | 36 | self.assertEqual(0, item.quality) 37 | self.assertEqual(4, item.sell_in) 38 | 39 | def test_it_updates_brie_items_before_the_sell_date(self): 40 | shop = gilded_rose.GildedRose() 41 | item = shop.of('Aged Brie', 10, 5) 42 | item.tick() 43 | 44 | self.assertEqual(11, item.quality) 45 | self.assertEqual(4, item.sell_in) 46 | 47 | def test_it_updates_brie_items_before_the_sell_date_with_maximum_quality(self): 48 | shop = gilded_rose.GildedRose() 49 | item = shop.of('Aged Brie', 50, 5) 50 | item.tick() 51 | 52 | self.assertEqual(50, item.quality) 53 | self.assertEqual(4, item.sell_in) 54 | 55 | def test_it_updates_brie_items_on_the_sell_date(self): 56 | shop = gilded_rose.GildedRose() 57 | item = shop.of('Aged Brie', 10, 0) 58 | item.tick() 59 | 60 | self.assertEqual(12, item.quality) 61 | self.assertEqual(-1, item.sell_in) 62 | 63 | def test_it_updates_brie_items_on_the_sell_date_near_maximum_quality(self): 64 | shop = gilded_rose.GildedRose() 65 | item = shop.of('Aged Brie', 49, 0) 66 | item.tick() 67 | 68 | self.assertEqual(50, item.quality) 69 | self.assertEqual(-1, item.sell_in) 70 | 71 | def test_it_updates_brie_items_on_the_sell_date_with_maximum_quality(self): 72 | shop = gilded_rose.GildedRose() 73 | item = shop.of('Aged Brie', 50, 0) 74 | item.tick() 75 | 76 | self.assertEqual(50, item.quality) 77 | self.assertEqual(-1, item.sell_in) 78 | 79 | def test_it_updates_brie_items_after_the_sell_date(self): 80 | shop = gilded_rose.GildedRose() 81 | item = shop.of('Aged Brie', 10, -10) 82 | item.tick() 83 | 84 | self.assertEqual(12, item.quality) 85 | self.assertEqual(-11, item.sell_in) 86 | 87 | def test_it_updates_brie_items_after_the_sell_date_with_maximum_quality(self): 88 | shop = gilded_rose.GildedRose() 89 | item = shop.of('Aged Brie', 50, -10) 90 | item.tick() 91 | 92 | self.assertEqual(50, item.quality) 93 | self.assertEqual(-11, item.sell_in) 94 | 95 | def test_it_updates_sulfuras_items_before_the_sell_date(self): 96 | shop = gilded_rose.GildedRose() 97 | item = shop.of('Sulfuras, Hand of Ragnaros', 10, 5) 98 | item.tick() 99 | 100 | self.assertEqual(10, item.quality) 101 | self.assertEqual(5, item.sell_in) 102 | 103 | def test_it_updates_sulfuras_items_on_the_sell_date(self): 104 | shop = gilded_rose.GildedRose() 105 | item = shop.of('Sulfuras, Hand of Ragnaros', 10, 5) 106 | item.tick() 107 | 108 | self.assertEqual(10, item.quality) 109 | self.assertEqual(5, item.sell_in) 110 | 111 | def test_it_updates_sulfuras_items_after_the_sell_date(self): 112 | shop = gilded_rose.GildedRose() 113 | item = shop.of('Sulfuras, Hand of Ragnaros', 10, -1) 114 | item.tick() 115 | 116 | self.assertEqual(10, item.quality) 117 | self.assertEqual(-1, item.sell_in) 118 | 119 | def test_it_updates_backstage_pass_items_long_before_the_sell_date(self): 120 | shop = gilded_rose.GildedRose() 121 | item = shop.of( 122 | 'Backstage passes to a TAFKAL80ETC concert', 123 | 10, 124 | 11 125 | ) 126 | item.tick() 127 | 128 | self.assertEqual(11, item.quality) 129 | self.assertEqual(10, item.sell_in) 130 | 131 | def test_it_updates_backstage_pass_items_close_to_the_sell_date(self): 132 | shop = gilded_rose.GildedRose() 133 | item = shop.of( 134 | 'Backstage passes to a TAFKAL80ETC concert', 135 | 10, 136 | 10 137 | ) 138 | item.tick() 139 | 140 | self.assertEqual(12, item.quality) 141 | self.assertEqual(9, item.sell_in) 142 | 143 | def test_it_updates_backstage_pass_items_close_to_the_sell_data_at_max_quality(self): 144 | shop = gilded_rose.GildedRose() 145 | item = shop.of( 146 | 'Backstage passes to a TAFKAL80ETC concert', 147 | 50, 148 | 10 149 | ) 150 | item.tick() 151 | 152 | self.assertEqual(50, item.quality) 153 | self.assertEqual(9, item.sell_in) 154 | 155 | def test_it_updates_backstage_pass_items_very_close_to_the_sell_date(self): 156 | shop = gilded_rose.GildedRose() 157 | item = shop.of( 158 | 'Backstage passes to a TAFKAL80ETC concert', 159 | 10, 160 | 5 161 | ) 162 | item.tick() 163 | 164 | self.assertEqual(13, item.quality) 165 | self.assertEqual(4, item.sell_in) 166 | 167 | def test_it_updates_backstage_pass_items_very_close_to_the_sell_date_at_max_quality(self): 168 | shop = gilded_rose.GildedRose() 169 | item = shop.of( 170 | 'Backstage passes to a TAFKAL80ETC concert', 171 | 50, 172 | 5 173 | ) 174 | item.tick() 175 | 176 | self.assertEqual(50, item.quality) 177 | self.assertEqual(4, item.sell_in) 178 | 179 | def test_it_updates_backstage_pass_items_with_one_day_left_to_sell(self): 180 | shop = gilded_rose.GildedRose() 181 | item = shop.of( 182 | 'Backstage passes to a TAFKAL80ETC concert', 183 | 10, 184 | 1 185 | ) 186 | item.tick() 187 | 188 | self.assertEqual(13, item.quality) 189 | self.assertEqual(0, item.sell_in) 190 | 191 | def test_it_updates_backstage_pass_items_with_one_day_left_to_sell_at_max_quality(self): 192 | shop = gilded_rose.GildedRose() 193 | item = shop.of( 194 | 'Backstage passes to a TAFKAL80ETC concert', 195 | 50, 196 | 1 197 | ) 198 | item.tick() 199 | 200 | self.assertEqual(50, item.quality) 201 | self.assertEqual(0, item.sell_in) 202 | 203 | def test_it_updates_backstage_pass_items_on_the_sell_date(self): 204 | shop = gilded_rose.GildedRose() 205 | item = shop.of( 206 | 'Backstage passes to a TAFKAL80ETC concert', 207 | 10, 208 | 0 209 | ) 210 | item.tick() 211 | 212 | self.assertEqual(0, item.quality) 213 | self.assertEqual(-1, item.sell_in) 214 | 215 | def test_it_updates_backstage_pass_items_after_the_sell_date(self): 216 | shop = gilded_rose.GildedRose() 217 | item = shop.of( 218 | 'Backstage passes to a TAFKAL80ETC concert', 219 | 10, 220 | -1 221 | ) 222 | item.tick() 223 | 224 | self.assertEqual(0, item.quality) 225 | self.assertEqual(-2, item.sell_in) 226 | 227 | def test_it_updates_conjured_items_before_the_sell_date(self): 228 | shop = gilded_rose.GildedRose() 229 | item = shop.of('Conjured Juna Cake', 10, 10) 230 | item.tick() 231 | 232 | self.assertEqual(8, item.quality) 233 | self.assertEqual(9, item.sell_in) 234 | 235 | def test_it_updates_conjured_items_at_zero_quality(self): 236 | shop = gilded_rose.GildedRose() 237 | item = shop.of('Conjured Juna Cake', 0, 10) 238 | item.tick() 239 | 240 | self.assertEqual(0, item.quality) 241 | self.assertEqual(9, item.sell_in) 242 | 243 | def test_it_updates_conjured_items_on_the_sell_date(self): 244 | shop = gilded_rose.GildedRose() 245 | item = shop.of('Conjured Juna Cake', 10, 0) 246 | item.tick() 247 | 248 | self.assertEqual(6, item.quality) 249 | self.assertEqual(-1, item.sell_in) 250 | 251 | def test_it_updates_conjured_items_on_the_sell_date_at_0_quality(self): 252 | shop = gilded_rose.GildedRose() 253 | item = shop.of('Conjured Juna Cake', 0, 0) 254 | item.tick() 255 | 256 | self.assertEqual(0, item.quality) 257 | self.assertEqual(-1, item.sell_in) 258 | 259 | def test_it_updates_conjured_items_after_the_sell_date(self): 260 | shop = gilded_rose.GildedRose() 261 | item = shop.of('Conjured Juna Cake', 10, -10) 262 | item.tick() 263 | 264 | self.assertEqual(6, item.quality) 265 | self.assertEqual(-11, item.sell_in) 266 | 267 | def test_it_updates_conjured_items_after_the_sell_date_at_zero_quality(self): 268 | shop = gilded_rose.GildedRose() 269 | item = shop.of('Conjured Juna Cake', 0, -10) 270 | item.tick() 271 | 272 | self.assertEqual(0, item.quality) 273 | self.assertEqual(-11, item.sell_in) 274 | -------------------------------------------------------------------------------- /tests/prime_factors_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import prime_factors 2 | from unittest_data_provider import data_provider 3 | import unittest 4 | 5 | 6 | class PrimeFactorsTest(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.generator = prime_factors.PrimeFactors() 10 | 11 | def checks(): 12 | return [ 13 | [1, []], 14 | [2, [2]], 15 | [3, [3]], 16 | [4, [2, 2]], 17 | [5, [5]], 18 | [6, [2, 3]], 19 | [8, [2, 2, 2]], 20 | [9, [3, 3]], 21 | [100, [2, 2, 5, 5]] 22 | ] 23 | 24 | @data_provider(checks) 25 | def test_it_generates_prime_factors(self, number, factors): 26 | self.assertEqual(factors, self.generator.generate(number)) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/read_write_json_test.py: -------------------------------------------------------------------------------- 1 | from src.katas.read_write_json import write_to_json_file, read_from_json_file 2 | import unittest 3 | 4 | 5 | class ReadWriteJsonTest(unittest.TestCase): 6 | 7 | _data = { 8 | "president": { 9 | "name": "Zaphod Beeblebrox", 10 | "species": "Betelgeusian" 11 | } 12 | } 13 | 14 | def test_can_write_given_data_to_json_file_in_proper_json_format(self): 15 | json_file = 'tests/fixtures/data_file.json' 16 | write_to_json_file(self._data, json_file) 17 | self.assertEqual(self._data, read_from_json_file(json_file)) 18 | -------------------------------------------------------------------------------- /tests/roman_numerals_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import roman_numerals 2 | from unittest_data_provider import data_provider 3 | import unittest 4 | 5 | 6 | class RomanNumeralsTest(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.generator = roman_numerals.RomanNumerals() 10 | 11 | def checks(): 12 | return [ 13 | [1, 'I'], 14 | [2, 'II'], 15 | [3, 'III'], 16 | [4, 'IV'], 17 | [5, 'V'], 18 | [6, 'VI'], 19 | [7, 'VII'], 20 | [8, 'VIII'], 21 | [9, 'IX'], 22 | [10, 'X'], 23 | [40, 'XL'], 24 | [50, 'L'], 25 | [90, 'XC'], 26 | [100, 'C'], 27 | [400, 'CD'], 28 | [500, 'D'], 29 | [900, 'CM'], 30 | [1000, 'M'], 31 | [3999, 'MMMCMXCIX'], 32 | ] 33 | 34 | @data_provider(checks) 35 | def test_it_generates_roman_numerals(self, number, numeral): 36 | self.assertEqual(numeral, self.generator.generate(number)) 37 | 38 | def test_it_cannot_generate_a_roman_numeral_for_less_than_1(self): 39 | self.assertFalse(self.generator.generate(0)) 40 | 41 | def test_it_cannot_generate_a_roman_numeral_for_more_than_3999(self): 42 | self.assertFalse(self.generator.generate(4000)) 43 | 44 | 45 | if __name__ == '__main__': 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /tests/string_calculator_test.py: -------------------------------------------------------------------------------- 1 | from src.katas import string_calculator 2 | from src.katas.exceptions import IllegalArgumentError 3 | import unittest 4 | 5 | 6 | class StringCalculatorTest(unittest.TestCase): 7 | 8 | def test_it_evaluates_an_empty_string_to_0(self): 9 | calculator = string_calculator.StringCalculator() 10 | 11 | self.assertEqual(0, calculator.add('')) 12 | 13 | def test_it_finds_the_sum_of_a_single_number(self): 14 | calculator = string_calculator.StringCalculator() 15 | 16 | self.assertEqual(5, calculator.add('5')) 17 | 18 | def test_it_finds_the_sum_of_two_number(self): 19 | calculator = string_calculator.StringCalculator() 20 | 21 | self.assertEqual(10, calculator.add('5, 5')) 22 | 23 | def test_it_finds_the_sum_of_any_amount_of_numbers(self): 24 | calculator = string_calculator.StringCalculator() 25 | 26 | self.assertEqual(19, calculator.add('5, 5, 5, 4')) 27 | 28 | def test_it_accepts_a_new_line_character_as_a_delimeter_too(self): 29 | calculator = string_calculator.StringCalculator() 30 | 31 | self.assertEqual(10, calculator.add("5\n5")) 32 | 33 | def test_negative_numbers_are_not_allowed(self): 34 | calculator = string_calculator.StringCalculator() 35 | 36 | with self.assertRaises(IllegalArgumentError): 37 | calculator.add('5, -4') 38 | 39 | def test_numbers_bigger_than_1000_are_ignored(self): 40 | calculator = string_calculator.StringCalculator() 41 | 42 | self.assertEqual(5, calculator.add('5, 1001')) 43 | -------------------------------------------------------------------------------- /tests/tennis_match_test.py: -------------------------------------------------------------------------------- 1 | from src.katas.tennis import tennis_match 2 | from unittest_data_provider import data_provider 3 | import unittest 4 | 5 | 6 | class TennisMatchTest(unittest.TestCase): 7 | 8 | def scores(): 9 | return [ 10 | [0, 0, 'love-love'], 11 | [1, 0, 'fifteen-love'], 12 | [1, 1, 'fifteen-fifteen'], 13 | [2, 0, 'thirty-love'], 14 | [3, 0, 'forty-love'], 15 | [2, 2, 'thirty-thirty'], 16 | [3, 3, 'deuce'], 17 | [4, 4, 'deuce'], 18 | [5, 5, 'deuce'], 19 | [4, 3, 'Advantage: John'], 20 | [3, 4, 'Advantage: Jane'], 21 | [4, 0, 'Winner: John'], 22 | [0, 4, 'Winner: Jane'], 23 | ] 24 | 25 | @data_provider(scores) 26 | def test_it_generates_prime_factors(self, player_one, player_two, score): 27 | john = tennis_match.Player('John') 28 | jane = tennis_match.Player('Jane') 29 | game = tennis_match.Game(john, jane) 30 | 31 | index1 = 0 32 | while index1 < player_one: 33 | game.points_to(john.get_name()) 34 | 35 | index1 += 1 36 | 37 | index2 = 0 38 | while index2 < player_two: 39 | game.points_to(jane.get_name()) 40 | 41 | index2 += 1 42 | 43 | self.assertEqual(score, game.score()) 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/unique_string_finder_test.py: -------------------------------------------------------------------------------- 1 | from src.katas.unique_string_finder import UniqueStringFinder 2 | from pprint import pprint 3 | import unittest 4 | 5 | 6 | class UniqueStringTest(unittest.TestCase): 7 | 8 | def test_it_finds_the_unique_string_from_list_of_strings(self): 9 | finder = UniqueStringFinder() 10 | 11 | self.assertEqual( 12 | finder.find_unique(['Aa', 'aaa', 'aaaaa', 'BbBb', 'Aaaa', 'AaAaAa', 'a']), 13 | 'BbBb' 14 | ) 15 | self.assertEqual( 16 | finder.find_unique(['abc', 'acb', 'bac', 'foo', 'bca', 'cab', 'cba']), 17 | 'foo' 18 | ) 19 | self.assertEqual(finder.find_unique([' ', 'a', ' ']), 'a') 20 | --------------------------------------------------------------------------------