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
|
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__(self) -> str:
result = self.date.strftime('%Y/%m/%d')
result += " " + self.comment + "\n"
for trans in self.transactions:
if len(trans) == 2:
# TODO: `price` must have a specific format!
account, price = trans
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.append(
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
|