read_file.py (4320B) - raw
1 from datetime import date, datetime
2 import re
3 import os
4
5
6 VALID_DATES_FORMAT = r'\d{4}[-./]\d{1,2}[-./]\d{1,2}'
7 VALID_DATES_SEP = '[-./]'
8
9
10 class entry:
11 def __init__(self, date: str, comment: str, transactions: list) -> None:
12 self.date = self.__date_from_str(date)
13 self.comment = comment
14 self.transactions = transactions
15
16
17 def __date_from_str(self, date_str: str):
18 """
19 Searches for a valid date on a string and transforms it into ISO
20 format. (YYYY-MM-DD)
21 """
22 my_date = re.findall(VALID_DATES_FORMAT, date_str)[0]
23 year, month, day = re.split(VALID_DATES_SEP, my_date)
24
25 return datetime.fromisoformat(f'{int(year)}-{int(month):02}-{int(day):02}')
26
27
28 def __str_to_price_format(self, price: str):
29 # Find the first instance of a number. A string of digits that may have
30 # a dot and more digits after.
31 price_nu = re.findall(r'\d+(?:\.\d*)?', price)[0]
32 # For the currency symbol, we get rid of the number.
33 price_sy = price.replace(price_nu, '')
34
35 if '-' in price:
36 # If there was a minus (-), add it to the number and delete it from
37 # the currency symbol.
38 price_nu = f"-{price_nu}"
39 price_sy = price_sy.replace('-', '')
40
41 # Remove the whitespace around the currency symbol.
42 price_sy = price_sy.strip()
43
44 # If the symbol is 1 character long, write it on the left.
45 if len(price_sy) == 1:
46 return f"{price_sy}{float(price_nu):.02f}"
47 # If it is longer than 1 character, write it on the right.
48 else:
49 return f"{float(price_nu):.02f} {price_sy}"
50
51
52 def __str__(self) -> str:
53 result = self.date.strftime('%Y/%m/%d')
54 result += " " + self.comment + "\n"
55
56 for trans in self.transactions:
57 if len(trans) == 2:
58 account, price = trans
59 price = self.__str_to_price_format(price)
60 else:
61 account = trans[0]
62 price = ""
63
64 result += f" {account:<35} {price:>12}\n"
65
66 return result
67
68
69 def give_me_file_contents(path: str):
70 line_comments = ";#%|*"
71 try:
72 with open(path, 'r', encoding='utf8') as fp:
73 result = fp.readlines()
74
75 for line in result:
76 # If line is just empty spaces or empty, ignore it.
77 if len(line.lstrip()) == 0:
78 continue
79
80 # If first character of line is a line comment, ignore it.
81 first_char = line.lstrip()[0]
82 if first_char in line_comments:
83 continue
84
85 yield line.strip()
86
87 except:
88 raise Exception(f"Error while trying to read {path} file.")
89
90
91 def is_new_entry(line: str):
92 """
93 Returns `True` if the line contains at least one valid date. This means
94 we're looking at a new transaction.
95 """
96 return re.search(VALID_DATES_FORMAT, line) is not None
97
98
99 def read_ledger(path: str):
100 files_to_read = [path]
101 date = None
102 comment = None
103 transactions = None
104
105 results = []
106
107 while files_to_read:
108 current_file = files_to_read.pop()
109
110 for line in give_me_file_contents(current_file):
111 if line.startswith('!include'):
112 file_path = line.split()[-1]
113 base_dir = os.path.dirname(current_file)
114 files_to_read.insert(0,
115 os.path.join(base_dir, file_path)
116 )
117
118 continue
119
120 if is_new_entry(line) and date is not None:
121 results.append(
122 entry(date, comment, transactions)
123 )
124
125 if is_new_entry(line):
126 date, comment = line.split(maxsplit=1)
127 transactions = []
128 else:
129 # The line is a new transaction
130 tabs_to_spaces = line.replace('\t', ' ')
131 transactions.append(
132 re.split(r'\s{2,}', tabs_to_spaces)
133 )
134
135 if date is not None:
136 results.append(
137 entry(date, comment, transactions)
138 )
139 date = None
140 comment = None
141 transactions = None
142
143 return results