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