aboutsummaryrefslogtreecommitdiff
path: root/utils/read_file.py
blob: 4f5e9abfee58a339ef4337c76d728678bda3fea3 (plain) (blame)
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