--- id: 5e44413e903586ffb414c94e title: Застосунок для ведення бюджету challengeType: 23 forumTopicId: 462361 dashedName: budget-app --- # --description-- Завершіть клас `Category`. Він повинен опрацьовувати об’єкти, базуючись на різних категоріях (наприклад, *їжа*, *одяг* та *розваги*). Коли об’єкти створені, вони передаються до назви категорії. Клас повинен мати поле класу під назвою `ledger`, яке є списком. Клас також повинен містити наступні методи: - Метод `deposit`, що приймає суму та опис. Якщо жодного опису не дано, то він автоматично стає порожнім рядком. Цей метод повинен додавати об’єкти до списку головної книги у вигляді `{"amount": amount, "description": description}`. - Метод `withdraw` схожий до методу `deposit`, але сума, яку він передає, повинна зберігатися у списку як від’ємне число. Якщо недостатньо коштів, то нічого додавати не потрібно. Цей метод повинен повертати `True`, якщо зняття дійсне, а в іншому випадку повертати `False`. - Метод `get_balance`, що повертає поточний баланс категорії бюджету на основі депозитів і зняття. - Метод `transfer`, що приймає суму або іншу бюджетну категорію як аргументи. Цей метод повинен додавати виведення суми та опис "Transfer to [Destination Budget Category]". Потім він повинен додати депозит до іншої категорії, що містить суму та опис "Transfer from [Source Budget Category]". Якщо коштів недостатньо, то нічого додавати не потрібно. Цей метод повинен повертати `True`, якщо переказ дійсний, а в іншому випадку повертати `False`. - Метод `check_funds`, що приймає суму як аргумент. Він повертає `False`, якщо сума більша за баланс бюджетної категорії, а в іншому випадку повертає `True`. Цей метод варто використовувати обом методам `withdraw` та `transfer`. Якщо бюджет надрукований, то повинні відображатися: - Рядок заголовка із 30 символів, де назва категорії знаходиться у центрі рядка символів `*`. - Список елементів у записі. Кожен рядок повинен показувати опис і суму. Повинні зображатися перші 23 символи опису, а потім сума. Сума повинна бути вирівняною за правим краєм, містити два десяткові знаки та відображати максимум 7 символів. - Рядок, що показує загальну категорію. Ось приклад використання: ```py food = Category("Food") food.deposit(1000, "deposit") food.withdraw(10.15, "groceries") food.withdraw(15.89, "restaurant and more food for dessert") clothing = Category("Clothing") food.transfer(50, clothing) print(food) ``` І ось приклад виводу: ```bash *************Food************* initial deposit 1000.00 groceries -10.15 restaurant and more foo -15.89 Transfer to Clothing -50.00 Total: 923.96 ``` Окрім класу `Category`, створіть функцію під назвою `create_spend_chart` (за межами класу), яка приймає список категорій як аргумент. Вона повинна повертати рядок зі стовпчиковою діаграмою. Діаграма повинна показувати відсотки, витрачені в кожній категорії, переданій до функції. Відсоток витраченого повинен бути розрахований лише за рахунок зняття, але не з депозитів. Внизу лівої частини графіку повинні бути мітки 0 - 100. Стовпчики у стовпчиковій діаграмі повинні бути створені із символу «o». Висота кожного стовпчика повинна бути округлена до найближчого десятка. Горизонтальна лінія під стовпчиками повинна розташовуватись в двох пробілах від кінцевого стовпчика. Назва кожної категорії повинна бути написана вертикально під стовпчиком. Вгорі повинен бути заголовок "Percentage spent by category". Ця функція буде тестуватися чотирма категоріями. Уважно розгляньте приклад виводу нижче і переконайтеся, що інтервал виводу повністю відповідає прикладу. ```bash Percentage spent by category 100| 90| 80| 70| 60| o 50| o 40| o 30| o 20| o o 10| o o o 0| o o o ---------- F C A o l u o o t d t o h i n g ``` # --hints-- Метод `deposit` має створити певний об’єкт у змінній екземпляра головної книги. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_deposit(self): self.food.deposit(900, "deposit") actual = self.food.ledger[0] expected = {"amount": 900, "description": "deposit"} self.assertEqual(actual, expected, 'Expected "deposit" method to create a specific object in the ledger instance variable.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Виклик методу `deposit` без опису має створити порожній опис. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_deposit_no_description(self): self.food.deposit(45.56) actual = self.food.ledger[0] expected = {"amount": 45.56, "description": ""} self.assertEqual(actual, expected, 'Expected calling "deposit" method with no description to create a blank description.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `withdraw` має створити певний об’єкт у змінній екземпляра `ledger`. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_withdraw(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") actual = self.food.ledger[1] expected = {"amount": -45.67, "description": "milk, cereal, eggs, bacon, bread"} self.assertEqual(actual, expected, 'Expected "withdraw" method to create a specific object in the ledger instance variable.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Виклик методу `withdraw` без опису має створити порожній опис. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_withdraw_no_description(self): self.food.deposit(900, "deposit") good_withdraw = self.food.withdraw(45.67) actual = self.food.ledger[1] expected = {"amount": -45.67, "description": ""} self.assertEqual(actual, expected, 'Expected "withdraw" method with no description to create a blank description.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `withdraw` має повернути `True`, якщо зняття відбулося. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_withdraw_no_description(self): self.food.deposit(900, "deposit") good_withdraw = self.food.withdraw(45.67) self.assertEqual(good_withdraw, True, 'Expected "withdraw" method to return "True".') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Виклик `food.deposit(900, "deposit")` та `food.withdraw(45.67, "milk, cereal, eggs, bacon, bread")` має повернути баланс `854.33`. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_get_balance(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") actual = self.food.get_balance() expected = 854.33 self.assertEqual(actual, expected, 'Expected balance to be 854.33') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Виклик методу `transfer` на об’єкті категорії має створити певний елемент головної книги в цьому об’єкті категорії. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") transfer_amount = 20 good_transfer = self.food.transfer(transfer_amount, self.entertainment) actual = self.food.ledger[2] expected = {"amount": -transfer_amount, "description": "Transfer to Entertainment"} self.assertEqual(actual, expected, 'Expected "transfer" method to create a specific ledger item in food object.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `transfer` має повернути `True`, якщо переказ відбувся. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") transfer_amount = 20 good_transfer = self.food.transfer(transfer_amount, self.entertainment) self.assertEqual(good_transfer, True, 'Expected "transfer" method to return "True".') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Виклик `transfer` на об’єкті категорії має зменшити баланс в об’єкті категорії. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") transfer_amount = 20 food_balance_before = self.food.get_balance() good_transfer = self.food.transfer(transfer_amount, self.entertainment) food_balance_after = self.food.get_balance() self.assertEqual(food_balance_before - food_balance_after, transfer_amount, 'Expected "transfer" method to reduce balance in food object.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `transfer` має збільшити баланс об’єкта категорії, переданого як аргумент. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") transfer_amount = 20 entertainment_balance_before = self.entertainment.get_balance() good_transfer = self.food.transfer(transfer_amount, self.entertainment) entertainment_balance_after = self.entertainment.get_balance() self.assertEqual(entertainment_balance_after - entertainment_balance_before, transfer_amount, 'Expected "transfer" method to increase balance in entertainment object.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `transfer` має створити певний елемент головної книги в об’єкті категорії, переданого як аргумент. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") transfer_amount = 20 good_transfer = self.food.transfer(transfer_amount, self.entertainment) actual = self.entertainment.ledger[0] expected = {"amount": transfer_amount, "description": "Transfer from Food"} self.assertEqual(actual, expected, 'Expected "transfer" method to create a specific ledger item in entertainment object.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `check_funds` має повернути `False`, якщо сума, передана до методу, перевищує баланс категорії. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_check_funds(self): self.food.deposit(10, "deposit") actual = self.food.check_funds(20) expected = False self.assertEqual(actual, expected, 'Expected "check_funds" method to be False') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `check_funds` має повернути `True`, якщо сума, передана до методу, не перевищує баланс категорії. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_check_funds(self): self.food.deposit(10, "deposit") actual = self.food.check_funds(10) expected = True self.assertEqual(actual, expected, 'Expected "check_funds" method to be True') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `withdraw` має повернути `False`, якщо зняття не відбулося. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") def test_withdraw_no_funds(self): self.food.deposit(100, "deposit") good_withdraw = self.food.withdraw(100.10) self.assertEqual(good_withdraw, False, 'Expected "withdraw" method to return "False".') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Метод `transfer` має повернути `False`, якщо переказ не відбувся. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_transfer_no_funds(self): self.food.deposit(100, "deposit") good_transfer = self.food.transfer(200, self.entertainment) self.assertEqual(good_transfer, False, 'Expected "transfer" method to return "False".') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` Друк екземпляра `Category` має дати інше рядкове представлення об’єкта. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") def test_to_string(self): self.food.deposit(900, "deposit") self.food.withdraw(45.67, "milk, cereal, eggs, bacon, bread") self.food.transfer(20, self.entertainment) actual = str(self.food) expected = "*************Food*************\\ndeposit 900.00\\nmilk, cereal, eggs, bac -45.67\\nTransfer to Entertainme -20.00\\nTotal: 834.33" self.assertEqual(actual, expected, 'Expected different string representation of object.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() ` const out = __pyodide.runPython(testCode); assert(out); } }) ``` `create_spend_chart` має надрукувати інше представлення діаграми. Переконайтеся, що всі інтервали точні. ```js ({ test: () => { pyodide.FS.writeFile('/home/pyodide/budget.py', code); pyodide.FS.writeFile('/home/pyodide/test_module.py',` import unittest import budget from importlib import reload reload(budget) class UnitTests(unittest.TestCase): maxDiff = None def setUp(self): self.food = budget.Category("Food") self.entertainment = budget.Category("Entertainment") self.business = budget.Category("Business") def test_create_spend_chart(self): self.food.deposit(900, "deposit") self.entertainment.deposit(900, "deposit") self.business.deposit(900, "deposit") self.food.withdraw(105.55) self.entertainment.withdraw(33.40) self.business.withdraw(10.99) actual = budget.create_spend_chart([self.business, self.food, self.entertainment]) expected = "Percentage spent by category\\n100| \\n 90| \\n 80| \\n 70| o \\n 60| o \\n 50| o \\n 40| o \\n 30| o \\n 20| o o \\n 10| o o \\n 0| o o o \\n ----------\\n B F E \\n u o n \\n s o t \\n i d e \\n n r \\n e t \\n s a \\n s i \\n n \\n m \\n e \\n n \\n t " self.assertEqual(actual, expected, 'Expected different chart representation. Check that all spacing is exact.') `); const testCode = ` from unittest import main from importlib import reload import test_module reload(test_module) t = main(module='test_module', exit=False) t.result.wasSuccessful() `; const out = __pyodide.runPython(testCode); assert(out); } }) ``` # --seed-- ## --seed-contents-- ```py class Category: pass def create_spend_chart(categories): pass ``` # --solutions-- ```py class Category: def __init__(self, name): self.name = name self.ledger = [] self.balance = 0 self.spent = 0 def __str__(self): first_line = f'{self.name.center(30, "*")}\n' lines = '' total = f'Total: {format(self.balance, ".2f")}' for n in range(len(self.ledger)): descr = self.ledger[n]["description"][:23] am = format(float(self.ledger[n]["amount"]), ".2f")[:7] lines = lines + f'{descr:<23}{am:>7}\n' return f'{first_line}{lines}{total}' def deposit(self, amount, description=''): self.ledger.append({ 'amount': float(amount), 'description': description }) self.balance = self.balance + float(amount) def withdraw(self, amount, description=''): if self.check_funds(amount): self.ledger.append({ 'amount': -float(amount), 'description': description }) self.balance = self.balance - float(amount) self.spent = self.spent + float(amount) return True else: return False def get_balance(self): return self.balance def transfer(self, amount, category): if self.check_funds(amount): # withdraw self.ledger.append({ 'amount': -float(amount), 'description': f'Transfer to {category.name}' }) self.balance = self.balance - float(amount) # deposit category.deposit(amount, f'Transfer from {self.name}') return True else: return False def check_funds(self, amount): if float(amount) > self.balance: return False else: return True def create_spend_chart(categories): total_expenses = 0 obj = {} col1 = [] str = [] final_str = 'Percentage spent by category\n' label_max_length = 0 label_strings = [] for category in categories: total_expenses = total_expenses + category.spent obj[category.name] = {'expenses': category.spent} obj[category.name]['label'] = list(category.name) if len(obj[category.name]['label']) > label_max_length: label_max_length = len(obj[category.name]['label']) for category in categories: obj[category.name]['percent'] = ( (category.spent / total_expenses * 100) // 10) * 10 obj[category.name]['column'] = [] for i in range(0, 110, 10): if obj[category.name]['percent'] >= i: obj[category.name]['column'].insert(0, 'o') else: obj[category.name]['column'].insert(0, ' ') for i in range(0, 110, 10): col1.insert(0, i) for i in range(11): str.append("") for key in obj: str[i] += (f'{obj[key]["column"][i]} ') final_str += f'{col1[i]:>3}| {str[i]}\n' final_str += f' {"-"*(1+3*len(obj))}\n ' for i in range(label_max_length): label_strings.append(' ') for k in obj: if len(obj[k]['label']) < label_max_length: obj[k]['label'].extend( f'{" "*(label_max_length-len(obj[k]["label"]))}') label_strings[i] += f'{obj[k]["label"][i]} ' if i < label_max_length - 1: label_strings[i] += '\n ' final_str += label_strings[i] print(final_str) return (final_str) ```