aboutsummaryrefslogtreecommitdiff
path: root/utils/read_file.py
blob: ee8f06d44a39fd795845247d80cd5024d465368a (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
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.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