--- id: 5e44413e903586ffb414c94e title: 预算应用 challengeType: 23 forumTopicId: 462361 dashedName: budget-app --- # --description-- Complete the `Category` class. 它应该能够根据不同的预算类别实例化对象,例如 *食物* 、 *服装* 和 *娱乐* 。 创建对象时,它们以类别的名称传递。 该类应该有一个名为 `ledger` 的实例变量,它是一个列表。 该类还应包含以下方法: - 接受金额和描述的 `deposit` 方法。 如果没有给出描述,它应该默认为一个空字符串。 该方法应以 `{"amount": amount, "description": description}` 的形式将对象附加到账本列表。 - `withdraw` 方法类似于 `deposit` 方法,但传入的金额应作为负数存储在账本中。 如果没有足够的资金,则不应向账本添加任何内容。 如果取款发生,此方法应返回 `True`,否则返回 `False`。 - `get_balance` 方法,根据发生的存款和取款返回预算类别的当前余额。 - `transfer` 方法,它接受一个金额和另一个预算类别作为参数。 该方法应添加带有金额和描述 “Transfer to [目的地预算类别]”的提款。 然后,该方法应将存款添加到其他预算类别,其金额和描述为 “Transfer from [来源预算类别]”。 如果没有足够的资金,则不应向任一账本添加任何内容。 如果转账发生,此方法应返回 `True`,否则返回 `False`。 - 接受金额作为参数的 `check_funds` 方法。 如果金额大于预算类别的余额,则返回 `False`,否则返回 `True`。 `withdraw` 方法和 `transfer` 方法都应该使用此方法。 打印预算对象时,它应显示: - 30 个字符的标题行,类别名称居中在一行 `*` 字符中。 - 账本中的项目列表。 每行应显示描述和金额。 应显示描述的前 23 个字符,然后是金额。 金额应右对齐,包含两位小数,最多显示 7 个字符。 - 一行显示类别总数。 Here is an example usage: ```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) ``` And here is an example of the output: ```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”字符组成。 每个条形的高度应四舍五入到最接近的 10。 条形图下面的水平线应该超过最后一个条形图再多两个空格。 每个类别名称应垂直写在栏下方。 顶部应该有一个标题,上面写着“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-- The `deposit` method should create a specific object in the ledger instance variable. ```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); } }) ``` Calling the `deposit` method with no description should create a blank description. ```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); } }) ``` The `withdraw` method should create a specific object in the `ledger` instance variable. ```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); } }) ``` Calling the `withdraw` method with no description should create a blank description. ```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); } }) ``` The `withdraw` method should return `True` if the withdrawal took place. ```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); } }) ``` Calling `food.deposit(900, "deposit")` and `food.withdraw(45.67, "milk, cereal, eggs, bacon, bread")` should return a balance of `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); } }) ``` Calling the `transfer` method on a category object should create a specific ledger item in that category object. ```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); } }) ``` The `transfer` method should return `True` if the transfer took place. ```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); } }) ``` Calling `transfer` on a category object should reduce the balance in the category object. ```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); } }) ``` The `transfer` method should increase the balance of the category object passed as its argument. ```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); } }) ``` The `transfer` method should create a specific ledger item in the category object passed as its argument. ```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); } }) ``` The `check_funds` method should return `False` if the amount passed to the method is greater than the category balance. ```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); } }) ``` The `check_funds` method should return `True` if the amount passed to the method is not greater than the category balance. ```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); } }) ``` The `withdraw` method should return `False` if the withdrawal didn't take place. ```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); } }) ``` The `transfer` method should return `False` if the transfer didn't take place. ```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); } }) ``` Printing a `Category` instance should give a different string representation of the object. ```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` should print a different chart representation. Check that all spacing is exact. ```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) ```