-
Notifications
You must be signed in to change notification settings - Fork 0
/
budget.py
192 lines (159 loc) · 6.31 KB
/
budget.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""
Category class for working with budgets.
The method create_spend_chart can be used to plot a chart that show the percentage
spent in each category passed in to the function.
The percentage spent is calculated only with the withdrawals and not with deposits.
Calculated is it like the following:
The percentages spent in each category to the total sum spent in all categories.
ref: https://forum.freecodecamp.org/t/scientific-programming-budget-app/414111/2
"""
from dataclasses import (
dataclass,
field,
)
from typing import (
Dict,
List,
Union,
)
@dataclass
class Category:
"""This class represents budget for a categorie,
that can be specified during the instantiation.
"""
# class instance variables
# because the dataclass decorator is used we get cool perks like
# a __init__ , __repr__ and a __eq__ method automaticlly created
categorie: str
ledger: List[Dict[str, Union[str, int, float]]] = field(default_factory=list)
# like double entry bookkeeping
spent: float = 0.0
income: float = 0.0
def deposit(self, amount: Union[int, float], description: str = "") -> None:
"""Append a new deposit to the ledger list as a dictionary.
Args:
amount (Union[int, float]): Amount of money. No currency symbol.
description (str, optional): A good description for the deposit.
Defaults to "".
"""
self.ledger.append(
{"amount": round(float(amount), 2), "description": description}
)
self.income += amount
def withdraw(self, amount: Union[int, float], description: str = "") -> bool:
"""Append a withdraw to the ledger. But only if enough funds are available.
Args:
amount (Union[int, float]): Amount to withdraw
description (str, optional): A good description for the withdraw.
Defaults to "".
Returns:
bool: True if the withdraw is possible or flase when it isn't.
"""
if self.check_funds(amount):
self.ledger.append(
{
"amount": round(float(-amount), 2),
"description": description,
}
)
self.spent += amount
self.income -= amount
return True
else:
return False
def get_balance(self) -> float:
"""Get the stored balance of the ledger.
Returns:
float: Current balance.
"""
return round(float(self.income), 2)
def transfer(self, amount: Union[int, float], categorie_obj) -> bool:
"""Transfer money to another categorie object.
Args:
amount (Union[int, float]): Money to transfer.
categorie_obj ([type]): Transfer recipient.
Returns:
bool: True when the transfere is possible otherwiese false.
"""
if self.check_funds(amount):
self.withdraw(amount, f"Transfer to {categorie_obj.categorie}")
categorie_obj.deposit(amount, f"Transfer from {self.categorie}")
return True
else:
return False
def check_funds(self, amount: Union[int, float]) -> bool:
"""Check if the input value is more or less than the balance.
Args:
balance (Union[int, float]): Input balance to check.
Returns:
bool: True if the stored balance is more or equal or false when it isn't.
"""
if self.income >= round(float(amount), 2):
return True
else:
return False
def __str__(self):
"""String representation of the class object when print(str(class_obj))
is used.
"""
titel_row: str = self.categorie.center(30, "*") + "\n"
items: str = ""
for entry in self.ledger:
# take only the first 23 characters of the description if its less or
# more the length is still fixed to 23
# the amount is right aligned and a has a max length of 7 inc.
# 2 decimal point numbers
items += (
f"{entry.get('description')[0:23]:23}"
+ f"{entry.get('amount'):>7.2f}"
+ "\n"
)
return titel_row + items + "Total: " + f"{self.income:.2f}"
def create_spend_chart(categories: List) -> str:
"""Create a chart that to show the percentage spent in each category passed in to
the function.
The percentage spent is calculated only with the withdrawals and not with deposits.
Args:
categories (List): List of instantiated Category objects.
Returns:
str: Chart in a bar-ish style.
"""
plot: str = "Percentage spent by category\n"
# calculate the total of everything spent in all categories
total_spent: float = sum(x.spent for x in categories)
# create a list with the percentage values of the spending for every category
# with the help of the floor division operator
percentages: List[float] = [(x.spent / total_spent) // 0.01 for x in categories]
for p_value in range(100, -10, -10):
plot += str(p_value).rjust(3, " ") + "|"
for percentage in percentages:
# if the percentage of the categorie is equal or greater than
# the percentage value of the row add a new "bar"
# else append 3 spaces
if percentage >= p_value:
plot += " o "
else:
plot += " " * 3
plot += " \n"
# build the x axis
plot += " " * 4 + "-" * 3 * len(percentages) + "-\n"
# calculate the length of the longest categorie
longest_name: int = max(len(x.categorie) for x in categories)
for char in range(longest_name):
# prepend 4 spaces before every row
plot += " " * 4
# for every name
for name in categories:
# append the char to the row
if char < len(name.categorie):
plot += " " + name.categorie[char] + " "
else:
# or if no char append 3 spaces
plot += " " * 3
plot += " \n"
# because a new line is appended rstrip() is nessesary
# or the test failes
# because rstrip() strips all trailing whitespace
# 2 whitespaces must be added after the last appended value
plot = plot.rstrip() + " " * 2
return plot