#!/usr/bin/python # bal - reconciles a ledger text file (i.e. a bare-bones checkbook balancer). # # Accepts a ledger on standard input and prints the reconciled version on # standard output. # # Copyright (c) 2007 by Reid Priedhorsky, . # # This script is distributed under the terms of the GNU General Public # License; see http://www.gnu.org/licenses/gpl.txt for more information. # # Ledger files have six columns: Date, Memo, Deposit, Withdraw, Balance, and # Committed. They can appear in any order and have any widths; these parameters # are defined near the top of the file and remain constant for the remainder. # The definition consists of two lines, e.g.: # # Date Memo Deposit Withdraw Balance Committed # ----------- ------------------- --------- --------- --------- --------- # # Text encountered before these lines is copied to the output verbatim, and # everything after is considered part of the ledger, except for lines which # are empty or contain only whitespace. (These lines are copied verbatim to # the output and are ignored in calculations.) Columns must be separated by # two spaces. # # The program calculates Balance and Committed totals for each line. A line # is considered committed if any text appears in the Committed column. # # TODO: # # - In-place reconciliation # - Date sorting # - Commas in quantities # - Removal of two spaces between columns limitation # - More robust input parsing # - Better error reporting # import re import string import sys columns = {} line_length = 0 DATE = 'date' MEMO = 'memo' DEPOSIT = 'deposit' WITHDRAW = 'withdraw' BALANCE = 'balance' COMMIT = 'commit' class Column: def __init__(self, start, end, order): self.start = start self.end = end self.order = order class Line: def __init__(self, text): if len(text) < line_length: text = text + ' ' * (line_length - len(text)) for col in columns.keys(): start = columns[col].start end = columns[col].end field = text[start:end] if col == DATE: self.date = text[start:end] elif col == MEMO: self.memo = text[start:end] elif col == DEPOSIT: self.deposit = float(field + '0') elif col == WITHDRAW: self.withdraw = float(field + '0') elif col == COMMIT: if string.strip(field): self.commit = 1 else: self.commit = None def calc(self, balance, commit): if self.deposit: balance = balance + self.deposit if self.commit: commit = commit + self.deposit if self.withdraw: balance = balance - self.withdraw if self.commit: commit = commit - self.withdraw self.balance = balance if self.commit: self.commit = commit return (balance, commit) def __str__(self): e = [None] * 6 for col in (DATE, MEMO, DEPOSIT, WITHDRAW, BALANCE, COMMIT): i = columns[col].order start = columns[col].start end = columns[col].end if col == DATE: e[i] = self.date elif col == MEMO: e[i] = self.memo elif col == DEPOSIT: if (self.deposit): e[i] = '%*.2f' % (end - start, self.deposit) else: e[i] = ' ' * (end - start) elif col == WITHDRAW: if (self.withdraw): e[i] = '%*.2f' % (end - start, self.withdraw) else: e[i] = ' ' * (end - start) elif col == BALANCE: e[i] = '%*.2f' % (end - start, self.balance) elif col == COMMIT: if self.commit is not None: e[i] = '%*.2f' % (end - start, self.commit) else: e[i] = ' ' * (end - start) ret = '' for i in range(len(e)): ret = ret + e[i] if i < len(e) - 1: ret = ret + ' ' return string.rstrip(ret) def main(): lines = sys.stdin.readlines() cur = 0 # Skip header lines. while 1: m = re.match(r'(-+)\s+(-+)\s+(-+)\s+(-+)\s+(-+)\s+(-+)$', lines[cur]) if m: break sys.stdout.write(lines[cur]) cur = cur + 1 sys.stdout.write(lines[cur]) # Determine column order & size global columns global line_length order = string.split(lines[cur - 1]) start = 0 end = None for i in range(len(order)): name = string.lower(order[i]) if name in (DATE, MEMO, DEPOSIT, WITHDRAW, BALANCE, COMMIT): columns[name] = Column(m.start(i + 1), m.end(i + 1), i) if m.end(i) > line_length: line_length = m.end(i) else: print 'Unknown column name: %s' % (order[i]) sys.exit(1) cur = cur + 1 # Mainloop: do calculations balance = 0.00 commit = 0.00 while cur < len(lines): if re.match(r'^\s*$', lines[cur]): sys.stdout.write(lines[cur]) else: line = Line(lines[cur]) (balance, commit) = line.calc(balance, commit) print line cur = cur + 1 if __name__ == '__main__': main()