aboutsummaryrefslogblamecommitdiff
path: root/utils/read_file.py
blob: 4f5e9abfee58a339ef4337c76d728678bda3fea3 (plain) (tree)


























                                                                                   























                                                                               





                                               
                                      
                                                         





















































                                                                           
                                       




























                                                           
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