- if load_from_file and os.path.exists(self.db_file):
- with open(self.db_file, "r") as f:
- self.parse_lines(f.readlines())
-
- def parse_lines(self, lines):
- import datetime
- import decimal
- inside_booking = False
- date_string, description = None, None
- booking_lines = []
- start_line = 0
- for i, line in enumerate(lines):
- prefix = f"line {i}"
- # we start with the case of an utterly empty line
- self.comments += [""]
- stripped_line = line.rstrip()
- if stripped_line == '':
- if inside_booking:
- # assume we finished a booking, finalize, and commit to DB
- if len(booking_lines) < 2:
- raise HandledException(f"{prefix} booking ends to early")
- booking = Booking(date_string, description, booking_lines, start_line)
- self.bookings += [booking]
- # expect new booking to follow so re-zeroall booking data
- inside_booking = False
- date_string, description = None, None
- booking_lines = []
- continue
- # if non-empty line, first get comment if any, and commit to DB
- split_by_comment = stripped_line.split(sep=";", maxsplit=1)
- if len(split_by_comment) == 2:
- self.comments[i] = split_by_comment[1]
- # if pre-comment empty: if inside_booking, this must be a comment-only line so we keep it for later ledger-output to capture those comments; otherwise, no more to process for this line
- non_comment = split_by_comment[0].rstrip()
- if non_comment.rstrip() == '':
- if inside_booking:
- booking_lines += ['']
- continue
- # if we're starting a booking, parse by first-line pattern
- if not inside_booking:
- start_line = i
- toks = non_comment.split(maxsplit=1)
- date_string = toks[0]
- try:
- datetime.datetime.strptime(date_string, '%Y-%m-%d')
- except ValueError:
- raise HandledException(f"{prefix} bad date string: {date_string}")
- try:
- description = toks[1]
- except IndexError:
- raise HandledException(f"{prefix} bad description: {description}")
- inside_booking = True
- continue
- # otherwise, read as transfer data
- toks = non_comment.split() # ignore specification's allowance of single spaces in names
- if len(toks) > 3:
- raise HandledException(f"{prefix} too many booking line tokens: {toks}")
- amount, currency = None, None
- account_name = toks[0]
- if account_name[0] == '[' and account_name[-1] == ']':
- # ignore specification's differentiation of "virtual" accounts
- account_name = account_name[1:-1]
- decimal_chars = ".-0123456789"
- if len(toks) == 3:
- i_currency = 1
- try:
- amount = decimal.Decimal(toks[1])
- i_currency = 2
- except decimal.InvalidOperation:
- try:
- amount = decimal.Decimal(toks[2])
- except decimal.InvalidOperation:
- raise HandledException(f"{prefix} no decimal number in: {toks[1:]}")
- currency = toks[i_currency]
- if currency[0] in decimal_chars:
- raise HandledException(f"{prefix} currency starts with int, dot, or minus: {currency}")
- elif len(toks) == 2:
- value = toks[1]
- inside_amount = False
- inside_currency = False
- amount_string = ""
- currency = ""
- dots_counted = 0
- for i, c in enumerate(value):
- if i == 0:
- if c in decimal_chars:
- inside_amount = True
- else:
- inside_currency = True
- if inside_currency:
- if c in decimal_chars and len(amount_string) == 0:
- inside_currency = False
- inside_amount = True
- else:
- currency += c
- continue
- if inside_amount:
- if c not in decimal_chars:
- if len(currency) > 0:
- raise HandledException(f"{prefix} amount has non-decimal chars: {value}")
- inside_currency = True
- inside_amount = False
- currency += c
- continue
- if c == '-' and len(amount_string) > 1:
- raise HandledException(f"{prefix} amount has non-start '-': {value}")
- if c == '.':
- if dots_counted > 1:
- raise HandledException(f"{prefix} amount has multiple dots: {value}")
- dots_counted += 1
- amount_string += c
- if len(amount_string) == 0:
- raise HandledException(f"{prefix} amount missing: {value}")
- if len(currency) == 0:
- raise HandledException(f"{prefix} currency missing: {value}")
- amount = decimal.Decimal(amount_string)
- booking_lines += [(account_name, amount, currency)]