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
|
from datetime import date, datetime
import re
import os
VALID_DATES_FORMAT = r'\d{4}[-./]\d{1,2}[-./]\d{1,2}'
VALID_DATES_SEP = '[-./]'
class entry:
def __init__(self, date: str, comment: str, transactions: list) -> None:
self.date = self.__date_from_str(date)
self.comment = comment
self.transactions = transactions
def __date_from_str(self, date_str: str):
"""
Searches for a valid date on a string and transforms it into ISO
format. (YYYY-MM-DD)
"""
my_date = re.findall(VALID_DATES_FORMAT, date_str)[0]
year, month, day = re.split(VALID_DATES_SEP, my_date)
return datetime.fromisoformat(f'{int(year)}-{int(month):02}-{int(day):02}')
def __str_to_price_format(self, price: str):
# Find the first instance of a number. A string of digits that may have
# a dot and more digits after.
price_nu = re.findall(r'\d+(?:\.\d*)?', price)[0]
# For the currency symbol, we get rid of the number.
price_sy = price.replace(price_nu, '')
if '-' in price:
# If there was a minus (-), add it to the number and delete it from
# the currency symbol.
price_nu = f"-{price_nu}"
price_sy = price_sy.replace('-', '')
# Remove the whitespace around the currency symbol.
price_sy = price_sy.strip()
# If the symbol is 1 character long, write it on the left.
if len(price_sy) == 1:
return f"{price_sy}{float(price_nu):.02f}"
# If it is longer than 1 character, write it on the right.
else:
return f"{float(price_nu):.02f} {price_sy}"
def __str__(self) -> str:
result = self.date.strftime('%Y/%m/%d')
result += " " + self.comment + "\n"
for trans in self.transactions:
if len(trans) == 2:
account, price = trans
price = self.__str_to_price_format(price)
else:
account = trans[0]
price = ""
result += f" {account:<35} {price:>12}\n"
return result
def give_me_file_contents(path: str):
line_comments = ";#%|*"
try:
with open(path, 'r', encoding='utf8') as fp:
result = fp.readlines()
for line in result:
# If line is just empty spaces or empty, ignore it.
if len(line.lstrip()) == 0:
continue
# If first character of line is a line comment, ignore it.
first_char = line.lstrip()[0]
if first_char in line_comments:
continue
yield line.strip()
except:
raise Exception(f"Error while trying to read {path} file.")
def is_new_entry(line: str):
"""
Returns `True` if the line contains at least one valid date. This means
we're looking at a new transaction.
"""
return re.search(VALID_DATES_FORMAT, line) is not None
def read_ledger(path: str):
files_to_read = [path]
date = None
comment = None
transactions = None
results = []
while files_to_read:
current_file = files_to_read.pop()
for line in give_me_file_contents(current_file):
if line.startswith('!include'):
file_path = line.split()[-1]
base_dir = os.path.dirname(current_file)
files_to_read.insert(0,
os.path.join(base_dir, file_path)
)
continue
if is_new_entry(line) and date is not None:
results.append(
entry(date, comment, transactions)
)
if is_new_entry(line):
date, comment = line.split(maxsplit=1)
transactions = []
else:
# The line is a new transaction
tabs_to_spaces = line.replace('\t', ' ')
transactions.append(
re.split(r'\s{2,}', tabs_to_spaces)
)
if date is not None:
results.append(
entry(date, comment, transactions)
)
date = None
comment = None
transactions = None
return results
|