curriculum/challenges/english/blocks/lab-budget-app/5e44413e903586ffb414c94e.md
In this lab, you will build a simple budget app that tracks spending in different categories and can show the relative spending percentage on a graph.
Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.
User Stories:
You should have a Category class that accepts a name as the argument.
The Category class should have an instance attribute ledger that is a list, and contains the list of transactions.
The Category class should have the following methods:
deposit method that accepts an amount and an optional description. If no description is given, it should default to an empty string. The method should append an object to the ledger list in the form of {'amount': amount, 'description': description}.withdraw method that accepts an amount and an optional description (default to an empty string). The method should store in ledger the amount passed in as a negative number, and should return True if the withdrawal succeeded and False otherwise.get_balance method that returns the current category balance based on ledger.transfer method that accepts an amount and another Category instance, withdraws the amount with description Transfer to [Destination], deposits it into the other category with description Transfer from [Source], where [Destination] and [Source] should be replaced by the name of destination and source categories. The method should return True when the transfer is successful, and False otherwise.check_funds method that accepts an amount and returns False if it exceeds the balance or True otherwise. This method must be used by both the withdraw and transfer methods.When a Category object is printed, it should:
* characters.ledger entry with up to 23 characters of its description left-aligned and the amount right-aligned (two decimal places, max 7 characters).Total: [balance], where [balance] should be replaced by the category total.Here is an example usage:
food = Category('Food')
food.deposit(1000, 'initial 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:
*************Food*************
initial deposit 1000.00
groceries -10.15
restaurant and more foo -15.89
Transfer to Clothing -50.00
Total: 923.96
You should have a function outside the Category class named create_spend_chart(categories) that returns a bar-chart string. To build the chart:
Percentage spent by category.100 down to 0 in steps of 10.o characters for the bars.This function will be tested with up to four categories.
Make sure to match the spacing of the example output exactly:
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
NOTE: open the browser console with F12 to see a more verbose output of the tests.
The deposit method should create a specific object in the ledger instance variable.
({
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 = runPython(testCode);
assert(out);
}
})
Calling the deposit method with no description should create a blank description.
({
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 = runPython(testCode);
assert(out);
}
})
The withdraw method should create a specific object in the ledger instance variable.
({
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 = runPython(testCode);
assert(out);
}
})
Calling the withdraw method with no description should create a blank description.
({
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 = runPython(testCode);
assert(out);
}
})
The withdraw method should return True if the withdrawal took place.
({
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 = 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.
({
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 = runPython(testCode);
assert(out);
}
})
Calling the transfer method on a category object should create a specific ledger item in that category object.
({
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 = runPython(testCode);
assert(out);
}
})
The transfer method should return True if the transfer took place.
({
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 = runPython(testCode);
assert(out);
}
})
Calling transfer on a category object should reduce the balance in the category object.
({
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 = runPython(testCode);
assert(out);
}
})
The transfer method should increase the balance of the category object passed as its argument.
({
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 = runPython(testCode);
assert(out);
}
})
The transfer method should create a specific ledger item in the category object passed as its argument.
({
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 = runPython(testCode);
assert(out);
}
})
The check_funds method should return False if the amount passed to the method is greater than the category balance.
({
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 = 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.
({
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 = runPython(testCode);
assert(out);
}
})
The withdraw method should return False if the withdrawal didn't take place.
({
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 = runPython(testCode);
assert(out);
}
})
The transfer method should return False if the transfer didn't take place.
({
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 = runPython(testCode);
assert(out);
}
})
Printing a Category instance should give a different string representation of the object.
({
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 = runPython(testCode);
assert(out);
}
})
Title at the top of create_spend_chart chart should say Percentage spent by category.
({
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_create_spend_chart(self):
self.food.deposit(900, "deposit")
self.food.withdraw(105.55)
chart = budget.create_spend_chart([self.food])
expected = "Percentage spent by category"
self.assertEqual(chart.split("\\n")[0], expected, "Chart should have correct title.")
`);
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 = runPython(testCode);
assert(out);
}
})
create_spend_chart chart should have correct percentages down the left side.
({
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_create_spend_chart(self):
self.food.deposit(900, "deposit")
self.food.withdraw(105.55)
chart = budget.create_spend_chart([self.food])
percentages = ["100|", " 90|", " 80|", " 70|", " 60|", " 50|", " 40|", " 30|", " 20|", " 10|", " 0|"]
for line, percent in zip(chart.split("\\n")[1:], percentages):
self.assertTrue(line.startswith(percent), "Chart correct percentages in the vertical axis.")
`);
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 = runPython(testCode);
assert(out);
}
})
The height of each bar on the create_spend_chart chart should be rounded down to the nearest 10.
({
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")
self.food.deposit(900, "deposit")
self.entertainment.deposit(900, "deposit")
self.business.deposit(900, "deposit")
self.food.withdraw(78)
self.entertainment.withdraw(22)
self.business.withdraw(8)
def test_create_spend_chart_rounding_close_to_upper_and_lower_ten(self):
chart_lines = budget.create_spend_chart([self.food, self.entertainment]).split("\\n")[1:12]
result_lines = '''100|
90|
80|
70| o
60| o
50| o
40| o
30| o
20| o o
10| o o
0| o o'''.split("\\n")
self.assertEqual(len(chart_lines), len(result_lines), "Lines missing in chart.")
for actual, expected in zip(chart_lines, result_lines):
self.assertTrue(actual.startswith(expected), "Expected different rounding of bars.")
def test_create_spend_chart_rounding_single_digit(self):
chart_lines = budget.create_spend_chart([self.business, self.food, self.entertainment]).split("\\n")[1:12]
result_lines = '''100|
90|
80|
70| o
60| o
50| o
40| o
30| o
20| o o
10| o o
0| o o o'''.split("\\n")
self.assertEqual(len(chart_lines), len(result_lines), "Lines missing in chart.")
for actual, expected in zip(chart_lines, result_lines):
self.assertTrue(actual.startswith(expected), "Expected different rounding of bars.")
`);
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 = runPython(testCode);
assert(out);
}
})
Each line in create_spend_chart chart should have the same length. Bars for different categories should be separated by two spaces, with additional two spaces after the final bar.
({
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")
self.food.deposit(900, "deposit")
self.entertainment.deposit(900, "deposit")
self.business.deposit(900, "deposit")
self.food.withdraw(78)
self.entertainment.withdraw(22)
self.business.withdraw(8)
def test_create_spend_chart_chart_lines_have_expected_length(self):
chart_categories = [[self.food, self.entertainment], [self.business, self.food, self.entertainment]]
expected_lengths = [len(line) for line in [" 0| o o ", " 0| o o o "]]
expected_chart_lines = 11
for categories, expected_length in zip(chart_categories, expected_lengths):
chart_lines = budget.create_spend_chart(categories).split("\\n")[1:12]
self.assertEqual(len(chart_lines), expected_chart_lines, "Lines missing in chart.")
for actual in chart_lines:
self.assertEqual(len(actual), expected_length, "Expected different length of the chart line. 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 = runPython(testCode);
assert(out);
}
})
create_spend_chart should correctly show horizontal line below the bars. Using three - characters for each category, and in total going two characters past the final bar.
({
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")
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)
def test_create_spend_chart_horizontal_bar(self):
chart_categories = [[self.business], [self.business, self.food], [self.business, self.food, self.entertainment]]
horizontal_lines = [" ----", " -------", " ----------"]
for categories, expected in zip(chart_categories, horizontal_lines):
actual = budget.create_spend_chart(categories).split("\\n")[12]
self.assertEqual(actual, expected, "Expected different horizontal bar. 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 = runPython(testCode);
assert(out);
}
})
create_spend_chart chart should not have new line character at the end.
({
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_no_ending_new_line(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])
self.assertFalse(actual.endswith("\\n"), "Expected chart to not have new line at the end.")
`);
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 = runPython(testCode);
assert(out);
}
})
create_spend_chart chart should have each category name written vertically below the bar. Each line should have the same length, each category should be separated by two spaces, with additional two spaces after the final category.
({
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")
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)
def test_create_spend_chart_names_two_categories(self):
chart = budget.create_spend_chart([self.food, self.entertainment])
actual = "\\n".join(chart.split("\\n")[13:]).rstrip("\\n")
expected = " F E \\n o n \\n o t \\n d e \\n r \\n t \\n a \\n i \\n n \\n m \\n e \\n n \\n t "
self.assertEqual(actual, expected, "Expected different category names written vertically below the bar. Check that all spacing is exact.")
def test_create_spend_chart_names_three_categories(self):
chart = budget.create_spend_chart([self.business, self.food, self.entertainment])
actual = "\\n".join(chart.split("\\n")[13:]).rstrip("\\n")
expected = " 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 category names written vertically below the bar. 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 = runPython(testCode);
assert(out);
}
})
create_spend_chart should print a different chart representation. Check that all spacing is exact. Open your browser console with F12 for more details.
({
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 = runPython(testCode);
assert(out);
}
})
class Category:
pass
def create_spend_chart(categories):
pass
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)