Build a Budget App Project - Build a Budget App Project

Tell us what’s happening:

the last test fails:
I’m generating the following error in the console on the last test:

ERROR: *test_create_spend_chart (test_module.UnitTests.test_create_spend_chart)* 
Traceback (most recent call last):
File "/home/pyodide/test_module.py", line 21, in test_create_spend_chart
actual = **budget.create_spend_chart([self.business, self.food, self.entertainment])** 
AttributeError: module 'budget' has no attribute 'create_spend_chart'

Your code so far

class Category:


    def __str__(self): 
        asterisks = (30 - len(self.category)) // 2   
  
        ret_string =  '*' * asterisks + self.category + '*' * asterisks + '\n'
        for row in self.ledger:  
            amt =  '{:.2f}'.format(row['amount']).rjust(7)
            if len(row['description']) > 22 :
                 amt = '{:.2f}'.format(row['amount']).rjust(6) 
            ret_string +=  "{0:<22}".format(row['description'][0:23]) + ' ' +  amt + '\n' 
        ret_string += 'Total: '+ '{:.2f}'.format(self.get_balance() )  
        return ret_string 


    def __init__(self,category):
        self.ledger = [] 
        self.category = category
        self.chart = [
            ['  0|'],          
[' 10|'],         
[' 20|'] ,              
[' 30|'] ,               
[' 40|' ],            
[' 50|' ] ,          
[' 60|'],          
[' 70|' ],           
[' 80|'],        
[' 90|' ],      
['100|']   
        ]

   
    def create_spend_chart(self, categories):
        total_by_category = []
        category_percent = []
        category_total = 0
        longest = 0
        budget_chart_line = ''
        for cat in categories: 
            if len( cat.category) > longest:
                longest = len( cat.category)
        for cat in categories: 
            #total witdrawls
            total_by_category.append( \
                {"category": cat.category, "total": cat._get_bal_withdraws()})
            #print( 'category: ', cat.category  )
            category_total += cat._get_bal_withdraws()

        #for item  in  total_by_category:
         #   amt  = item["total"]
         #   category_percent = amt / category_total * 100  
           # print(f'{category_percent} = {amt} / {total} * 100 ')
           # item['percent'] =category_percent - (category_percent  % 10)
           # print(f'calc {category_percent}  % 10 = int {int(category_percent / 10)}')  
            #print(f"item['percent'] ={category_percent} - ({category_percent}  % 10)")
             
       
    
       
        col_switch = 0
        # calculate precent and assing to row 
        for item  in  total_by_category: 
            percent = int(item['total'] / category_total * 10)
           # print("new precct", 'total:', item['total'],'category_total',category_total,\
            # 'precent:' , percent, int(item['total'])) 
           
            col_format = ''
            for i in range(11) : 
                if (col_switch != 0):
                    col_format = "{0:>3}"
                else:
                    col_format = "{0:>2}" 
                # deal with percent 0 - 1  line '0|' has a value
                if  percent > i and  item['total'] > 0  or   percent == i and  item['total'] > 0  :
                    self.chart[i].append( col_format.format("o")) 
                else : 
                    self.chart[i].append( col_format.format(" ") ) 
            col_switch = 1

    # need to pass the right side of the chart to match the spaces
        for i in range(11) :
            self.chart[i].append("  ")
         
       
       # the first column  with 'o' has width of 2 and the rest 3 
        kluge = 0  
        #print('\nPercentage spent by category')
        budget_chart_line +='\nPercentage spent by category \n'
        for row in reversed(self.chart): 
            chart_line = ''
            for col in row: 
                if col != ' ':
                    if  col != '-' :
                        chart_line += col 
                    else: 
                        if kluge == 0:
                            chart_line += " {0:<1}".format(col)
                            kluge=1
                        else:
                            chart_line += (col * 3) 
                else:
                    chart_line +=  col 
            chart_line
  
            budget_chart_line += chart_line + '\n' 


#underline in chart
        underline = [' -', ]
        chart_line = '   '
        for i  in  range(len(total_by_category)): 
                 underline.append( "{0:>3}".format('-'* 3)) 
        for col in underline:
            chart_line +=   col 
        
        
        budget_chart_line += chart_line + '\n' 
        
#pivot names

        # [['F', 'o', 'o', 'd'], ['C', 'l', 'o', 't', 'h', 'i', 'n', 'g'], ['A', 'u', 't', 'o']]
        chart_cat = []  
        for i in range(longest):
            pivot_char = ['   ']
            for l in range(len(categories)):
                if (l != 0):
                    col_format = "{0:>3}"
                else:
                    col_format = "{0:>2}"

                if (i < len(total_by_category[l]["category"])):
                    pivot_char.append(col_format.format(total_by_category[l]["category"][i] ))
                else:
                    pivot_char.append(col_format.format(' '))  
            chart_cat.append(pivot_char) 


        chart_line = ' '
        for row in  chart_cat: 
            row.append('  ') 
        for row in  chart_cat:  
            for col in row:
                chart_line +=  col 
            chart_line+= '\n '

        budget_chart_line += chart_line   
        return budget_chart_line

#A deposit method vthat accepts an amount and 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}.
def deposit(self, amount, description = ‘’):
self.ledger.append({‘amount’: amount, ‘description’: description})

#A withdraw method that is similar to the deposit method, but the amount passed in should be stored in the ledger as a negative number. If there are not enough funds, nothing should be added to the ledger. This method should return True if the withdrawal took place, and False otherwise.

def withdraw(self,   amount,description='' ): 
    if amount > 0 and self.get_balance() > amount  :
        amount = amount * -1
        self.ledger.append({  'amount': amount, 'description': description})
        return True
    else:
        return False

#A get_balance method that returns the current balance of the budget category based on the deposits and withdrawals that have occurred.
def get_balance(self):
balance = 0.0
for row in self.ledger:
balance += row[‘amount’]
return balance

return

def _get_bal_withdraws(self) :
    withdraws = 0
    for row in self.ledger:
        if row['amount'] < 0 :
            withdraws += row['amount']  
    return withdraws * -1

#A transfer method that accepts an amount and another budget category as arguments. The method should add a withdrawal with the amount and the description ‘Transfer to [Destination Budget Category]’. The method should then add a deposit to the other budget category with the amount and the description ‘Transfer from [Source Budget Category]’. If there are not enough funds, nothing should be added to either ledgers. This method should return True if the transfer took place, and False otherwise.
def transfer(self, amount, Category):
if self.get_balance() > amount :
self.withdraw(amount,"Transfer to " + Category.category)
Category.deposit(amount,'Transfer from ’ + self.category)
return True
else:
return False

A check_funds method that accepts an amount as an argument. It returns False if the amount is greater than the balance of the budget category and returns True otherwise. This method should be used by both the withdraw method and transfer method.

def check_funds(self, amount):
    if amount > self.get_balance():
        return False
    return True

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)
clothing.withdraw(1.15, ‘groceries’)

print(food)
#print(clothing)
#print(auto)
#print(food.get_balance())

#print(clothing.get_balance())
print(food.create_spend_chart([food, clothing]))
#food.create_spend_chart([food, clothing, auto])


Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36

Challenge Information:

Build a Budget App Project - Build a Budget App Project

Besides the Category class, create a function (outside of the class) called create_spend_chart

create_spend_chart should not be part of the class, that’s why it can’t be found:

AttributeError: module 'budget' has no attribute 'create_spend_chart'

I moved to module outside the class and ran the tests.
I failed to understand the assertion ('Percentage spent by category\n100| ) and I’m not sure what output in the console I should compare.

Generate the following :
FAIL: test_create_spend_chart (test_module.UnitTests.test_create_spend_chart)
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9 ----------------------------------------------------------------------
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9 Traceback (most recent call last):
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9 File “/home/pyodide/test_module.py”, line 23, in test_create_spend_chart
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9 self.assertEqual(actual, expected, ‘Expected different chart representation. Check that all spacing is exact.’)
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9 AssertionError: None != 'Percentage spent by category\n100| [384 chars] t ’ : Expected different chart representation. Check that all spacing is exact.
batched @ pyodide.asm.js:9Understand this warning
pyodide.asm.js:9
batched @ pyodide.asm.js:9
pyodide.asm.js:9 ----------------------------------------------------------------------
the image is a better representation of the console.


the console output:
Food
pyodide.asm.js:9 initial deposit 1000.00
pyodide.asm.js:9 groceries -10.15
pyodide.asm.js:9 restaurant and more foo -15.89
pyodide.asm.js:9 Transfer to Clothing -50.00
pyodide.asm.js:9 Total: 923.96
pyodide.asm.js:9
pyodide.asm.js:9 Percentage spent by category
pyodide.asm.js:9 100|
pyodide.asm.js:9 90|
pyodide.asm.js:9 80|
pyodide.asm.js:9 70|
pyodide.asm.js:9 60| o
pyodide.asm.js:9 50| o
pyodide.asm.js:9 40| o
pyodide.asm.js:9 30| o
pyodide.asm.js:9 20| o o
pyodide.asm.js:9 10| o o o
pyodide.asm.js:9 0| o o o
pyodide.asm.js:9 ----------
pyodide.asm.js:9 F C A
pyodide.asm.js:9 o l u
pyodide.asm.js:9 o o t
pyodide.asm.js:9 d t o
pyodide.asm.js:9 h
pyodide.asm.js:9 i
pyodide.asm.js:9 n
pyodide.asm.js:9 g
pyodide.asm.js:9
pyodide.asm.js:9
pyodide.asm.js:9 Food
pyodide.asm.js:9 initial deposit 1000.00
pyodide.asm.js:9 groceries -10.15
pyodide.asm.js:9 restaurant and more foo -15.89
pyodide.asm.js:9 Transfer to Clothing -50.00
pyodide.asm.js:9 Total: 923.96
pyodide.asm.js:9
pyodide.asm.js:9 Percentage spent by category
pyodide.asm.js:9 100|
pyodide.asm.js:9 90|
pyodide.asm.js:9 80|
pyodide.asm.js:9 70|
pyodide.asm.js:9 60| o
pyodide.asm.js:9 50| o
pyodide.asm.js:9 40| o
pyodide.asm.js:9 30| o
pyodide.asm.js:9 20| o o
pyodide.asm.js:9 10| o o o
pyodide.asm.js:9 0| o o o
pyodide.asm.js:9 ----------
pyodide.asm.js:9 F C A
pyodide.asm.js:9 o l u
pyodide.asm.js:9 o o t
pyodide.asm.js:9 d t o
pyodide.asm.js:9 h
pyodide.asm.js:9 i
pyodide.asm.js:9 n
pyodide.asm.js:9 g
pyodide.asm.js:9
2pyodide.asm.js:9
pyodide.asm.js:9 Percentage spent by category
pyodide.asm.js:9 100|
pyodide.asm.js:9 90|
pyodide.asm.js:9 80|
pyodide.asm.js:9 70| o
pyodide.asm.js:9 60| o
pyodide.asm.js:9 50| o
pyodide.asm.js:9 40| o
pyodide.asm.js:9 30| o
pyodide.asm.js:9 20| o o
pyodide.asm.js:9 10| o o
pyodide.asm.js:9 0| o o o
pyodide.asm.js:9 ----------
pyodide.asm.js:9 B F E
pyodide.asm.js:9 u o n
pyodide.asm.js:9 s o t
pyodide.asm.js:9 i d e
pyodide.asm.js:9 n r
pyodide.asm.js:9 e t
pyodide.asm.js:9 s a
pyodide.asm.js:9 s i
pyodide.asm.js:9 n
pyodide.asm.js:9 m
pyodide.asm.js:9 e
pyodide.asm.js:9 n
pyodide.asm.js:9 t
pyodide.asm.js:9

The code:
class Category:

def __str__(self): 
    asterisks = (30 - len(self.category)) // 2   

    ret_string =  '*' * asterisks + self.category + '*' * asterisks + '\n'
    for row in self.ledger:  
        amt =  '{:.2f}'.format(row['amount']).rjust(7)
        if len(row['description']) > 22 :
             amt = '{:.2f}'.format(row['amount']).rjust(6) 
        ret_string +=  "{0:<22}".format(row['description'][0:23]) + ' ' +  amt + '\n' 
    ret_string += 'Total: '+ '{:.2f}'.format(self.get_balance() )  
    return ret_string 


def __init__(self,category):
    self.ledger = [] 
    self.category = category

#A deposit method vthat accepts an amount and 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}.
def deposit(self, amount, description = ‘’):
self.ledger.append({‘amount’: amount, ‘description’: description})

#A withdraw method that is similar to the deposit method, but the amount passed in should be stored in the ledger as a negative number. If there are not enough funds, nothing should be added to the ledger. This method should return True if the withdrawal took place, and False otherwise.

def withdraw(self,   amount,description='' ): 
    if amount > 0 and self.get_balance() > amount  :
        amount = amount * -1
        self.ledger.append({  'amount': amount, 'description': description})
        return True
    else:
        return False

#A get_balance method that returns the current balance of the budget category based on the deposits and withdrawals that have occurred.
def get_balance(self):
balance = 0.0
for row in self.ledger:
balance += row[‘amount’]
return balance

return

def _get_bal_withdraws(self) :
    withdraws = 0
    for row in self.ledger:
        if row['amount'] < 0 :
            withdraws += row['amount']  
    return withdraws * -1

#A transfer method that accepts an amount and another budget category as arguments. The method should add a withdrawal with the amount and the description ‘Transfer to [Destination Budget Category]’. The method should then add a deposit to the other budget category with the amount and the description ‘Transfer from [Source Budget Category]’. If there are not enough funds, nothing should be added to either ledgers. This method should return True if the transfer took place, and False otherwise.
def transfer(self, amount, Category):
if self.get_balance() > amount :
self.withdraw(amount,"Transfer to " + Category.category)
Category.deposit(amount,'Transfer from ’ + self.category)
return True
else:
return False

A check_funds method that accepts an amount as an argument. It returns False if the amount is greater than the balance of the budget category and returns True otherwise. This method should be used by both the withdraw method and transfer method.

def check_funds(self, amount):
    if amount > self.get_balance():
        return False
    return True

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’)
clothing.deposit(1000, ‘initial deposit’)
auto= Category(‘Auto’)
auto.deposit(1000, ‘initial deposit’)
auto.withdraw(20.15, ‘groceries’)
food.transfer(50, clothing)
clothing.withdraw(10.15, ‘groceries’)
clothing.withdraw(20.15, ‘groceries’)
auto.deposit(1000, ‘deposit’)
print(food)
#print(clothing)
#print(auto)
#print(food.get_balance())
#print(clothing.get_balance())

def create_spend_chart( categories):
chart = [
[’ 0|‘],
[’ 10|‘],
[’ 20|‘] ,
[’ 30|‘] ,
[’ 40|’ ],
[’ 50|’ ] ,
[’ 60|‘],
[’ 70|’ ],
[’ 80|‘],
[’ 90|’ ],
[‘100|’]
]

    total_by_category = []
    category_percent = []
    total = 0
    longest = 0
    for cat in categories: 
        if len( cat.category) > longest:
            longest = len( cat.category)
    for cat in categories: 
        #total witdrawls
        total_by_category.append( \
            {"category": cat.category, "total": cat._get_bal_withdraws()})
        total += cat._get_bal_withdraws()

    for item  in  total_by_category:
        amt  = item["total"]
        category_percent = amt / total * 100  
        item['percent'] =category_percent - (category_percent  % 10)
         
   

   
    col_switch = 0
    for item  in  total_by_category:
        enum = int(item['percent'] / 10)   + 1 
       
        col_format = ''
        for i in range(11) : 
            if (col_switch != 0):
                col_format = "{0:>3}"
            else:
                col_format = "{0:>2}" 
            if  enum > i    :
                chart[i].append( col_format.format("o")) 
            else : 
                chart[i].append( col_format.format(" ") ) 
        col_switch = 1


    for i in range(11) :
        chart[i].append("  ")
     
   #print chart 
    kluge = 0  
    print('\nPercentage spent by category')
    for row in reversed(chart): 
        chart_line = ''
        for col in row: 
            if col != ' ':
                if  col != '-' :
                    chart_line += col 
                else: 
                    if kluge == 0:
                        chart_line += " {0:<1}".format(col)
                        kluge=1
                    else:
                        chart_line += (col * 3) 
            else:
                chart_line +=  col 
    
        print(chart_line)   
    
    remain_chart = []
     #underline 
    underline = [' -', ]
    line = '   '
    for i  in  range(len(total_by_category)): 
             underline.append( "{0:>3}".format('-'* 3)) 
    for col in underline:
        line +=   col 

    print(line)
    remain_chart.append(underline)  
    #print(longest)
    # [['F', 'o', 'o', 'd'], ['C', 'l', 'o', 't', 'h', 'i', 'n', 'g'], ['A', 'u', 't', 'o']]
    chart_cat = [] 
    col_switch = 0
    for i in range(longest):
        pivot_char = ['   ']
        for l in range(len(categories)):
            if (l != 0):
                col_format = "{0:>3}"
            else:
                col_format = "{0:>2}"

            if (i < len(total_by_category[l]["category"])):
                pivot_char.append(col_format.format(total_by_category[l]["category"][i] ))
            else:
                pivot_char.append(col_format.format(' '))  
        chart_cat.append(pivot_char) 
    line = ' '
    for row in  chart_cat: 
        row.append('  ') 
    for row in  chart_cat:  
        for col in row:
            line +=  col 
        line+= '\n '
        
    print(line,'\n')

create_spend_chart([food, clothing, auto])

copy & paste preview console:
Food
initial deposit 1000.00
groceries -10.15
restaurant and more foo -15.89
Transfer to Clothing -50.00
Total: 923.96

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

coy & paste console message:

class Category:
def str(self):
asterisks = (30 - len(self.category)) // 2

    ret_string =  '*' * asterisks + self.category + '*' * asterisks + '\n'
    for row in self.ledger:  
        amt =  '{:.2f}'.format(row['amount']).rjust(7)
        if len(row['description']) > 22 :
             amt = '{:.2f}'.format(row['amount']).rjust(6) 

// running tests
create_spend_chart should print a different chart representation. Check that all spacing is exact.
// tests completed

Please don’t post 500 lines of nonsense into your reply.

If you want to format code you can do it like this:

When you enter a code block into a forum post, please precede it with a separate line of three backticks and follow it with a separate line of three backticks to make it easier to read.

You can also use the “preformatted text” tool in the editor (</>) to add backticks around text.

See this post to find the backtick on your keyboard.
Note: Backticks (`) are not single quotes (').

Read your error messages. What do you think this means?

An assertion error gives you a lot of information to track down a problem. For example:

AssertionError: 'Year' != 'Years'

Your output comes first, and the output that the test expects is second.

AssertionError: ‘Year’ != ‘Years’

Your output: Year does not equal what’s expected: Years

Here’s another example:

E       AssertionError: Expected different output when calling "arithmetic_arranger()" with ["3801 - 2", "123 + 49"]
E       assert '  3801      123    \n   - 2     + 49    \n------    -----    \n' == '  3801      123\n-    2    +  49\n------    -----'

The first line is long, and it helps to view it as 2 lines in fixed width characters, so you can compare it character by character:

'  3801      123    \n   - 2     + 49    \n------    -----    \n'
'  3801      123\n-    2    +  49\n------    -----'

Again, your output is first and the expected output is second. Here it’s easy to see extra spaces or \n characters.

I hope this helps interpret your error!

sorry - my bad.
I should have carefully read the instructions.
Thanks for your quick response ::smiling_face:L

kind of stuck

the last test fails with ‘: Expected different chart representation. Check that all spacing is exact’


'AssertionError: 'Perc[21 chars]ory\n\n100| \n 90| \n 80| [371 chars] \n ’ != 'Perc[21 chars]ory\n100| \n 90| \n 80| [353 chars] t ‘’

i do not understand what is the test objection. Unless I’m reading it wrong, to me, all the lines match. Not sure where the numbers [371 chars] and [353 chars] are generated and why there are 18 char diff. The string returned from the method call has length 419.

Can someone please explain the output and the difference?

Read my previous post explaining how to interpret this error. Put it on two lines so you can compare easily.

'Perc[21 chars]ory\n\n100| \n 90| \n 80| [371 chars] \n ’ != 
'Perc[21 chars]ory\n100| \n 90| \n 80| [353 chars] t ‘’

The first line is yours, the second is the desired outputs. Try to spot where it gets different.

They don’t…