diff options
-rw-r--r-- | Makefile | 47 | ||||
-rw-r--r-- | amount.cc | 473 | ||||
-rw-r--r-- | balance.cc | 123 | ||||
-rw-r--r-- | gnucash.cc | 251 | ||||
-rw-r--r-- | ledger.cc | 124 | ||||
-rw-r--r-- | ledger.h | 270 | ||||
-rw-r--r-- | main.cc | 86 | ||||
-rw-r--r-- | parse.cc | 189 |
8 files changed, 1563 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a89f0586 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +CODE = amount.cc ledger.cc parse.cc gnucash.cc balance.cc +ifndef LIBRARY +CODE := $(CODE) main.cc +endif + +OBJS = $(patsubst %.cc,%.o,$(CODE)) + +CFLAGS = -Wall -ansi -pedantic +DFLAGS = -g +INCS = -I/usr/include/xmltok +LIBS = -lgmpxx -lgmp -lpcre -lxmlparse + +ifdef LIBRARY + +CFLAGS := $(CFLAGS) -fpic + +all: make.deps libledger.so ledger + +libledger.so: $(OBJS) + g++ $(CFLAGS) $(INCS) $(DFLAGS) -shared -fpic -o $@ $(OBJS) $(LIBS) + +ledger: main.cc + g++ $(INCS) $(DFLAGS) -o $@ main.cc -L. -lledger + +else # LIBRARY + +all: make.deps ledger + +ledger: $(OBJS) + g++ $(CFLAGS) $(INCS) $(DFLAGS) -o $@ $(OBJS) $(LIBS) + +endif # LIBRARY + +%.o: %.cc + g++ $(CFLAGS) $(INCS) $(DFLAGS) -c -o $@ $< + +clean: + rm -f libledger.so ledger *.o + +rebuild: clean deps all + +deps: make.deps + +make.deps: Makefile + cc -M $(INCS) $(CODE) main.cc > $@ + +include make.deps diff --git a/amount.cc b/amount.cc new file mode 100644 index 00000000..b6c26270 --- /dev/null +++ b/amount.cc @@ -0,0 +1,473 @@ +#include <sstream> +#include <cassert> + +#include <gmp.h> // GNU multi-precision library +#include <pcre.h> // Perl regular expression library + +#include "ledger.h" + +namespace ledger { + +////////////////////////////////////////////////////////////////////// +// +// The `amount' structure. Every transaction has an associated amount, +// which is represented by this structure. `amount' uses the GNU +// multi-precision library, allowing for arbitrarily large amounts. +// Each amount is a quantity of commodity at a certain price; the +// default commodity is the US dollar, with a price of 1.00. +// + +#define MAX_PRECISION 10 // must be 2 or higher + +class gmp_amount : public amount +{ + bool priced; + + mpz_t price; + commodity * price_comm; + + mpz_t quantity; + commodity * quantity_comm; + + gmp_amount(const gmp_amount& other) {} + gmp_amount& operator=(const gmp_amount& other) { return *this; } + + public: + gmp_amount() : priced(false), price_comm(NULL), quantity_comm(NULL) { + mpz_init(price); + mpz_init(quantity); + } + + virtual ~gmp_amount() { + mpz_clear(price); + mpz_clear(quantity); + } + + virtual const std::string& comm_symbol() const { + assert(quantity_comm); + return quantity_comm->symbol; + } + + virtual amount * copy() const { + gmp_amount * new_amt = new gmp_amount(); + new_amt->priced = priced; + mpz_set(new_amt->price, price); + new_amt->price_comm = price_comm; + mpz_set(new_amt->quantity, quantity); + new_amt->quantity_comm = quantity_comm; + return new_amt; + } + + virtual amount * value() const { + if (! priced) { + return copy(); + } else { + gmp_amount * new_amt = new gmp_amount(); + new_amt->priced = false; + multiply(new_amt->quantity, quantity, price); + new_amt->quantity_comm = price_comm; + return new_amt; + } + } + + virtual operator bool() const; + + virtual void credit(const amount * other) { + *this += *other; + } + virtual void operator+=(const amount& other); + + virtual void parse(const char * num) { + *this = num; + } + virtual amount& operator=(const char * num); + virtual operator std::string() const; + + static const std::string to_str(const commodity * comm, const mpz_t val); + + static void parse(mpz_t out, char * num); + static void round(mpz_t out, const mpz_t val, int prec); + static void multiply(mpz_t out, const mpz_t l, const mpz_t r); + + friend amount * create_amount(const char * value, const amount * price); +}; + +amount * create_amount(const char * value, const amount * price) +{ + gmp_amount * a = new gmp_amount(); + a->parse(value); + + // If a price was specified, it refers to a total price for the + // whole `value', meaning we must divide to determine the + // per-commodity price. + + if (price) { + assert(! a->priced); // don't specify price twice! + + const gmp_amount * p = dynamic_cast<const gmp_amount *>(price); + assert(p); + + // There is no need for per-commodity pricing when the total + // price is in the same commodity as the quantity! In that case, + // the two will always be identical. + if (a->quantity_comm == p->quantity_comm) { + assert(mpz_cmp(a->quantity, p->quantity) == 0); + return a; + } + + mpz_t quotient; + mpz_t remainder; + mpz_t addend; + + mpz_init(quotient); + mpz_init(remainder); + mpz_init(addend); + + mpz_ui_pow_ui(addend, 10, MAX_PRECISION); + + mpz_tdiv_qr(quotient, remainder, p->quantity, a->quantity); + mpz_mul(remainder, remainder, addend); + mpz_tdiv_q(remainder, remainder, a->quantity); + mpz_mul(quotient, quotient, addend); + mpz_add(quotient, quotient, remainder); + + a->priced = true; + mpz_set(a->price, quotient); + a->price_comm = p->quantity_comm; + + mpz_clear(quotient); + mpz_clear(remainder); + mpz_clear(addend); + } + return a; +} + +gmp_amount::operator bool() const +{ + mpz_t copy; + mpz_init_set(copy, quantity); + assert(quantity_comm); + gmp_amount::round(copy, copy, quantity_comm->precision); + bool zero = mpz_sgn(copy) == 0; + mpz_clear(copy); + return ! zero; +} + +const std::string gmp_amount::to_str(const commodity * comm, const mpz_t val) +{ + mpz_t copy; + mpz_t quotient; + mpz_t rquotient; + mpz_t remainder; + mpz_t divisor; + bool negative = false; + + mpz_init_set(copy, val); + + mpz_init(quotient); + mpz_init(rquotient); + mpz_init(remainder); + mpz_init(divisor); + + gmp_amount::round(copy, copy, comm->precision); + + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); + mpz_tdiv_qr(quotient, remainder, copy, divisor); + + if (mpz_sgn(quotient) < 0 || mpz_sgn(remainder) < 0) + negative = true; + mpz_abs(quotient, quotient); + mpz_abs(remainder, remainder); + + assert(MAX_PRECISION - comm->precision > 0); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - comm->precision); + mpz_tdiv_qr(rquotient, remainder, remainder, divisor); + + std::ostringstream s; + + if (comm->prefix) { + s << comm->symbol; + if (comm->separate) + s << " "; + } + + if (negative) + s << "-"; + s << quotient; + s << '.'; + + s.width(comm->precision); + s.fill('0'); + s << rquotient; + + if (! comm->prefix) { + if (comm->separate) + s << " "; + s << comm->symbol; + } + + mpz_clear(copy); + mpz_clear(quotient); + mpz_clear(rquotient); + mpz_clear(remainder); + mpz_clear(divisor); + + return s.str(); +} + +gmp_amount::operator std::string() const +{ + std::ostringstream s; + + assert(quantity_comm); + s << to_str(quantity_comm, quantity); + + if (priced) { + assert(price_comm); + s << " @ " << to_str(price_comm, price); + } + return s.str(); +} + +void gmp_amount::parse(mpz_t out, char * num) +{ + if (char * p = std::strchr(num, '/')) { + mpz_t numer; + mpz_t val; + + std::string numer_str(num, p - num); + mpz_init_set_str(numer, numer_str.c_str(), 10); + mpz_init(val); + + int missing = MAX_PRECISION - (std::strlen(++p) - 1); + assert(missing > 0); + mpz_ui_pow_ui(val, 10, missing); + + mpz_mul(out, numer, val); + + mpz_clear(numer); + mpz_clear(val); + } + else { + static char buf[256]; + + // jww (2003-09-28): What if there is no decimal? + + std::memset(buf, '0', 255); + std::strncpy(buf, num, std::strlen(num)); + + char * t = std::strchr(buf, '.'); + for (int prec = 0; prec < MAX_PRECISION; prec++) { + *t = *(t + 1); + t++; + } + *t = '\0'; + + mpz_set_str(out, buf, 10); + } +} + +amount& gmp_amount::operator=(const char * num) +{ + // Compile the regular expression used for parsing amounts + static pcre * re = NULL; + if (! re) { + const char *error; + int erroffset; + static const std::string amount_re = + "(([^-0-9/.]+)(\\s*))?([-0-9/.]+)((\\s*)([^-0-9/.@]+))?"; + const std::string regexp = + "^" + amount_re + "(\\s*@\\s*" + amount_re + ")?$"; + re = pcre_compile(regexp.c_str(), 0, &error, &erroffset, NULL); + } + + bool saw_commodity; + std::string symbol; + bool prefix; + bool separate; + int precision; + + static char buf[256]; + int ovector[60]; + int matched, result; + + matched = pcre_exec(re, NULL, num, std::strlen(num), 0, 0, ovector, 60); + if (matched > 0) { + saw_commodity = false; + + if (ovector[1 * 2] >= 0) { + // A prefix symbol was found + saw_commodity = true; + prefix = true; + separate = ovector[3 * 2] != ovector[3 * 2 + 1]; + result = pcre_copy_substring(num, ovector, matched, 2, buf, 255); + assert(result >= 0); + symbol = buf; + } + + // This is the value, and must be present + assert(ovector[4 * 2] >= 0); + result = pcre_copy_substring(num, ovector, matched, 4, buf, 255); + assert(result >= 0); + + // Determine the precision used + if (char * p = std::strchr(buf, '.')) + precision = std::strlen(++p); + else if (char * p = std::strchr(buf, '/')) + precision = std::strlen(++p) - 1; + else + precision = 0; + + // Parse the actual quantity + parse(quantity, buf); + + if (ovector[5 * 2] >= 0) { + // A suffix symbol was found + saw_commodity = true; + prefix = false; + separate = ovector[6 * 2] != ovector[6 * 2 + 1]; + result = pcre_copy_substring(num, ovector, matched, 7, buf, 255); + assert(result >= 0); + symbol = buf; + } + + if (! saw_commodity) { + quantity_comm = commodity_usd; + } else { + commodities_iterator item = commodities.find(symbol.c_str()); + if (item == commodities.end()) { + quantity_comm = new commodity(symbol, prefix, separate, precision); + std::pair<commodities_iterator, bool> insert_result = + commodities.insert(commodities_entry(symbol, quantity_comm)); + assert(insert_result.second); + } else { + quantity_comm = (*item).second; + + // If a finer precision was used than the commodity allows, + // increase the precision. + if (precision > quantity_comm->precision) + quantity_comm->precision = precision; + } + } + + // If the following succeeded, then we have a price + if (ovector[8 * 2] >= 0) { + saw_commodity = false; + + if (ovector[9 * 2] >= 0) { + // A prefix symbol was found + saw_commodity = true; + prefix = true; + separate = ovector[11 * 2] != ovector[11 * 2 + 1]; + result = pcre_copy_substring(num, ovector, matched, 10, buf, 255); + assert(result >= 0); + symbol = buf; + } + + assert(ovector[12 * 2] >= 0); + result = pcre_copy_substring(num, ovector, matched, 4, buf, 255); + assert(result >= 0); + + // Determine the precision used + if (char * p = std::strchr(buf, '.')) + precision = std::strlen(++p); + else if (char * p = std::strchr(buf, '/')) + precision = std::strlen(++p) - 1; + else + precision = 0; + + // Parse the actual price + parse(price, buf); + priced = true; + + if (ovector[13 * 2] >= 0) { + // A suffix symbol was found + saw_commodity = true; + prefix = false; + separate = ovector[14 * 2] != ovector[14 * 2 + 1]; + result = pcre_copy_substring(num, ovector, matched, 15, buf, 255); + assert(result >= 0); + symbol = buf; + } + + if (! saw_commodity) { + price_comm = commodity_usd; + } else { + commodities_iterator item = commodities.find(symbol.c_str()); + if (item == commodities.end()) { + price_comm = new commodity(symbol, prefix, separate, precision); + std::pair<commodities_iterator, bool> insert_result = + commodities.insert(commodities_entry(symbol, price_comm)); + assert(insert_result.second); + } else { + price_comm = (*item).second; + + // If a finer precision was used than the commodity allows, + // increase the precision. + if (precision > price_comm->precision) + price_comm->precision = precision; + } + } + } + } else { + std::cerr << "Failed to parse amount: " << num << std::endl; + } + return *this; +} + +void gmp_amount::operator+=(const amount& _other) +{ + const gmp_amount& other = dynamic_cast<const gmp_amount&>(_other); + assert(quantity_comm == other.quantity_comm); + mpz_add(quantity, quantity, other.quantity); +} + +void gmp_amount::round(mpz_t out, const mpz_t val, int prec) +{ + mpz_t divisor; + mpz_t quotient; + mpz_t remainder; + + mpz_init(divisor); + mpz_init(quotient); + mpz_init(remainder); + + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); + mpz_tdiv_qr(quotient, remainder, val, divisor); + + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec - 1); + mpz_mul_ui(divisor, divisor, 5); + if (mpz_cmp(remainder, divisor) >= 0) { + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); + mpz_sub(remainder, divisor, remainder); + mpz_add(out, val, remainder); + } else { + mpz_sub(out, val, remainder); + } + + mpz_clear(divisor); + mpz_clear(quotient); + mpz_clear(remainder); +} + +void gmp_amount::multiply(mpz_t out, const mpz_t l, const mpz_t r) +{ + mpz_t divisor; + + mpz_init(divisor); + + mpz_mul(out, l, r); + + // The number is at double-precision right now, so rounding at + // precision 0 effectively means rounding to the ordinary + // precision. + gmp_amount::round(out, out, 0); + + // after multiplying, truncate to the correct precision + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); + mpz_tdiv_q(out, out, divisor); + + mpz_clear(divisor); +} + +} // namespace ledger diff --git a/balance.cc b/balance.cc new file mode 100644 index 00000000..2c9569a4 --- /dev/null +++ b/balance.cc @@ -0,0 +1,123 @@ +#include <iostream> +#include <vector> + +#include <pcre.h> // Perl regular expression library + +#include "ledger.h" + +namespace ledger { + +////////////////////////////////////////////////////////////////////// +// +// Balance report. +// + +void report_balances(std::ostream& out, std::vector<entry *>& ledger, + bool show_children, bool show_empty) +{ +#if 0 + // Compile the list of specified regular expressions, which can be + // specified on the command line, or using an include/exclude file. + + std::list<pcre *> regexps; + + for (; optind < argc; optind++) { + const char *error; + int erroffset; + pcre * re = pcre_compile(argv[optind], PCRE_CASELESS, + &error, &erroffset, NULL); + assert(re); + regexps.push_back(re); + } +#endif + + // The balance of all accounts must equal zero + totals future_balance; + totals current_balance; + totals cleared_balance; + + std::cout.width(10); + std::cout << std::right << "Future" << " "; + std::cout.width(10); + std::cout << std::right << "Current" << " "; + std::cout.width(10); + std::cout << std::right << "Cleared" << std::endl; + + for (std::map<const std::string, account *>::iterator i = accounts.begin(); + i != accounts.end(); + i++) { + if (! show_empty && ! (*i).second->future) + continue; + + int depth = 0; + account * acct = (*i).second; + while (acct->parent) { + depth++; + acct = acct->parent; + } + +#if 0 + if (! regexps.empty()) { + bool matches = false; + for (std::list<pcre *>::iterator r = regexps.begin(); + r != regexps.end(); + r++) { + int ovector[30]; + if (pcre_exec(*r, NULL, (*i).first.c_str(), (*i).first.length(), + 0, 0, ovector, 30) >= 0) { + matches = true; + break; + } + } + + if (! matches) + continue; + } + else +#endif + if (! show_children && depth) { + continue; + } + + std::cout.width(10); + std::cout << (*i).second->future << " "; + std::cout.width(10); + std::cout << (*i).second->current << " "; + std::cout.width(10); + std::cout << (*i).second->cleared << " "; + + if (depth) { + while (--depth >= 0) + std::cout << " "; + std::cout << (*i).second->name << std::endl; + } else { + std::cout << (*i).first << std::endl; + +#if 0 + if (regexps.empty()) { +#endif + future_balance.credit((*i).second->future); + current_balance.credit((*i).second->current); + cleared_balance.credit((*i).second->cleared); +#if 0 + } +#endif + } + } + +#if 0 + if (regexps.empty()) { +#endif + // jww (2003-09-29): Let `totals' be streamed + future_balance.print(std::cout); + std::cout << " "; + current_balance.print(std::cout); + std::cout << " "; + cleared_balance.print(std::cout); + std::cout << std::endl; +#if 0 + } +#endif +} + +} // namespace ledger diff --git a/gnucash.cc b/gnucash.cc new file mode 100644 index 00000000..70d35de5 --- /dev/null +++ b/gnucash.cc @@ -0,0 +1,251 @@ +#include <sstream> +#include <vector> +#include <cstring> +#include <cassert> + +extern "C" { +#include <xmlparse.h> // expat XML parser +} + +#include "ledger.h" + +namespace ledger { + +static account * curr_account; +static std::string curr_account_id; +static entry * curr_entry; +static commodity * entry_comm; +static commodity * curr_comm; +static amount * curr_value; +static std::string curr_quant; +static XML_Parser current_parser; + +static std::vector<entry *> * current_ledger; + +enum { + NO_ACTION, + ACCOUNT_NAME, + ACCOUNT_ID, + ACCOUNT_PARENT, + COMM_SYM, + COMM_NAME, + COMM_PREC, + ENTRY_NUM, + ALMOST_ENTRY_DATE, + ENTRY_DATE, + ENTRY_DESC, + XACT_STATE, + XACT_AMOUNT, + XACT_VALUE, + XACT_QUANTITY, + XACT_ACCOUNT, + XACT_NOTE +} action; + +static void startElement(void *userData, const char *name, const char **atts) +{ + if (std::strcmp(name, "gnc:account") == 0) { + assert(! curr_account); + curr_account = new account(name); + } + else if (std::strcmp(name, "act:name") == 0) + action = ACCOUNT_NAME; + else if (std::strcmp(name, "act:id") == 0) + action = ACCOUNT_ID; + else if (std::strcmp(name, "act:parent") == 0) + action = ACCOUNT_PARENT; + else if (std::strcmp(name, "gnc:commodity") == 0) { + assert(! curr_comm); + curr_comm = new commodity; + } + else if (std::strcmp(name, "cmdty:id") == 0) + action = COMM_SYM; + else if (std::strcmp(name, "cmdty:name") == 0) + action = COMM_NAME; + else if (std::strcmp(name, "cmdty:fraction") == 0) + action = COMM_PREC; + else if (std::strcmp(name, "gnc:transaction") == 0) { + assert(! curr_entry); + curr_entry = new entry; + } + else if (std::strcmp(name, "trn:num") == 0) + action = ENTRY_NUM; + else if (std::strcmp(name, "trn:date-posted") == 0) + action = ALMOST_ENTRY_DATE; + else if (action == ALMOST_ENTRY_DATE && std::strcmp(name, "ts:date") == 0) + action = ENTRY_DATE; + else if (std::strcmp(name, "trn:description") == 0) + action = ENTRY_DESC; + else if (std::strcmp(name, "trn:split") == 0) { + assert(curr_entry); + curr_entry->xacts.push_back(new transaction()); + } + else if (std::strcmp(name, "split:reconciled-state") == 0) + action = XACT_STATE; + else if (std::strcmp(name, "split:amount") == 0) + action = XACT_AMOUNT; + else if (std::strcmp(name, "split:value") == 0) + action = XACT_VALUE; + else if (std::strcmp(name, "split:quantity") == 0) + action = XACT_QUANTITY; + else if (std::strcmp(name, "split:account") == 0) + action = XACT_ACCOUNT; + else if (std::strcmp(name, "split:memo") == 0) + action = XACT_NOTE; +} + + +static void endElement(void *userData, const char *name) +{ + if (std::strcmp(name, "gnc:account") == 0) { + assert(curr_account); + accounts.insert(accounts_entry(curr_account->name, curr_account)); + accounts.insert(accounts_entry(curr_account_id, curr_account)); + curr_account = NULL; + } + else if (std::strcmp(name, "gnc:commodity") == 0) { + assert(curr_comm); + commodities.insert(commodities_entry(curr_comm->symbol, curr_comm)); + curr_comm = NULL; + } + else if (std::strcmp(name, "gnc:transaction") == 0) { + assert(curr_entry); + if (! curr_entry->validate()) { + std::cerr << "Failed to balance the following transaction, " + << "ending on line " + << XML_GetCurrentLineNumber(current_parser) << std::endl; + curr_entry->print(std::cerr); + } else { + current_ledger->push_back(curr_entry); + } + curr_entry = NULL; + } + action = NO_ACTION; +} + +static void dataHandler(void *userData, const char *s, int len) +{ + switch (action) { + case ACCOUNT_NAME: + curr_account->name = std::string(s, len); + break; + + case ACCOUNT_ID: + curr_account_id = std::string(s, len); + break; + + case ACCOUNT_PARENT: { + accounts_iterator i = accounts.find(std::string(s, len)); + assert(i != accounts.end()); + curr_account->parent = (*i).second; + (*i).second->children.insert(account::pair(curr_account->name, + curr_account)); + break; + } + + case COMM_SYM: + if (curr_comm) + curr_comm->symbol = std::string(s, len); + else if (curr_account) + curr_account->comm = commodities[std::string(s, len)]; + else if (curr_entry) + entry_comm = commodities[std::string(s, len)]; + break; + + case COMM_NAME: + curr_comm->name = std::string(s, len); + break; + + case COMM_PREC: + curr_comm->precision = len - 1; + break; + + case ENTRY_NUM: + curr_entry->code = std::string(s, len); + break; + + case ENTRY_DATE: { + struct tm when; + strptime(std::string(s, len).c_str(), "%Y-%m-%d %H:%M:%S %z", &when); + curr_entry->date = std::mktime(&when); + break; + } + + case ENTRY_DESC: + curr_entry->desc = std::string(s, len); + break; + + case XACT_STATE: + curr_entry->cleared = (*s == 'y' || *s == 'c'); + break; + + case XACT_VALUE: { + assert(entry_comm); + std::string value = std::string(s, len) + " " + entry_comm->symbol; + curr_value = create_amount(value.c_str()); + break; + } + + case XACT_QUANTITY: + curr_quant = std::string(s, len); + break; + + case XACT_ACCOUNT: { + accounts_iterator i = accounts.find(std::string(s, len)); + assert(i != accounts.end()); + curr_entry->xacts.back()->acct = (*i).second; + + std::string value = curr_quant + " " + (*i).second->comm->symbol; + curr_entry->xacts.back()->cost = create_amount(value.c_str(), curr_value); + break; + } + + case XACT_NOTE: + curr_entry->xacts.back()->note = std::string(s, len); + break; + + case NO_ACTION: + case ALMOST_ENTRY_DATE: + case XACT_AMOUNT: + break; + + default: + assert(0); + break; + } +} + +bool parse_gnucash(std::istream& in, std::vector<entry *>& ledger) +{ + char buf[BUFSIZ]; + + XML_Parser parser = XML_ParserCreate(NULL); + current_parser = parser; + + //XML_SetUserData(parser, &depth); + XML_SetElementHandler(parser, startElement, endElement); + XML_SetCharacterDataHandler(parser, dataHandler); + + current_ledger = &ledger; + + curr_account = NULL; + curr_entry = NULL; + curr_comm = NULL; + + action = NO_ACTION; + + while (! in.eof()) { + in.getline(buf, BUFSIZ - 1); + if (! XML_Parse(parser, buf, std::strlen(buf), in.eof())) { + std::cerr << XML_ErrorString(XML_GetErrorCode(parser)) + << " at line " << XML_GetCurrentLineNumber(parser) + << std::endl; + return false; + } + } + XML_ParserFree(parser); + + return true; +} + +} // namespace ledger diff --git a/ledger.cc b/ledger.cc new file mode 100644 index 00000000..64a2381a --- /dev/null +++ b/ledger.cc @@ -0,0 +1,124 @@ +#include <vector> + +#include "ledger.h" + +namespace ledger { + +commodities_t commodities; +commodity * commodity_usd; + +accounts_t accounts; + +void entry::print(std::ostream& out) const +{ + char buf[32]; + std::strftime(buf, 31, "%Y.%m.%d ", std::localtime(&date)); + out << buf; + + if (cleared) + out << "* "; + if (! code.empty()) + out << '(' << code << ") "; + if (! desc.empty()) + out << " " << desc; + + out << std::endl; + + for (std::list<transaction *>::const_iterator i = xacts.begin(); + i != xacts.end(); + i++) { + out << " "; + + std::string acct_name; + for (account * acct = (*i)->acct; + acct; + acct = acct->parent) { + if (acct_name.empty()) + acct_name = acct->name; + else + acct_name = acct->name + ":" + acct_name; + } + + out.width(30); + out << std::left << acct_name << " "; + + out.width(10); + out << std::right << *((*i)->cost); + + if (! (*i)->note.empty()) + out << " ; " << (*i)->note; + + out << std::endl; + } +} + +bool entry::validate() const +{ + totals balance; + + for (std::list<transaction *>::const_iterator i = xacts.begin(); + i != xacts.end(); + i++) { + balance.credit((*i)->cost->value()); + } + + if (balance) { + std::cout << "Totals are:" << std::endl; + balance.print(std::cout); + } + return ! balance; // must balance to 0.0 +} + +void totals::credit(const totals& other) +{ + for (const_iterator_t i = other.amounts.begin(); + i != other.amounts.end(); + i++) { + credit((*i).second); + } +} + +totals::operator bool() const +{ + for (const_iterator_t i = amounts.begin(); i != amounts.end(); i++) + if (*((*i).second)) + return true; + return false; +} + +void totals::print(std::ostream& out) const +{ + for (const_iterator_t i = amounts.begin(); i != amounts.end(); i++) + std::cout << (*i).first << " = " << *((*i).second) << std::endl; +} + +amount * totals::value(const std::string& commodity) +{ + // Render all of the amounts into the given commodity. This + // requires known prices for each commodity. + + amount * total = create_amount((commodity + " 0.00").c_str()); + + for (iterator_t i = amounts.begin(); i != amounts.end(); i++) + *total += *((*i).second); + + return total; +} + +// Print out the entire ledger that was read in, but now sorted. +// This can be used to "wash" ugly ledger files. + +void print_ledger(std::ostream& out, std::vector<entry *>& ledger) +{ + // Sort the list of entries by date, then print them in order. + + std::sort(ledger.begin(), ledger.end(), cmp_entry_date()); + + for (std::vector<entry *>::const_iterator i = ledger.begin(); + i != ledger.end(); + i++) { + (*i)->print(out); + } +} + +} // namespace ledger diff --git a/ledger.h b/ledger.h new file mode 100644 index 00000000..0e261ceb --- /dev/null +++ b/ledger.h @@ -0,0 +1,270 @@ +#ifndef _LEDGER_H +#define _LEDGER_H "$Revision: 1.1 $" + +////////////////////////////////////////////////////////////////////// +// +// ledger: Double-entry ledger accounting +// +// by John Wiegley <johnw@newartisans.com> +// +// Copyright (c) 2003 New Artisans, Inc. All Rights Reserved. + +#include <iostream> +#include <string> +#include <list> +#include <map> +#include <ctime> + +namespace ledger { + +// Format of a ledger entry (GNUcash account files are also supported): +// +// DATE [CLEARED] (CODE) DESCRIPTION +// ACCOUNT AMOUNT [; NOTE] +// ACCOUNT AMOUNT [; NOTE] +// ... +// +// The DATE can be YYYY.MM.DD or YYYY/MM/DD or MM/DD. +// The CLEARED bit is a '*' if the account has been cleared. +// The CODE can be anything, but must be enclosed in parenthesis. +// The DESCRIPTION can be anything, up to a newline. +// +// The ACCOUNT is a colon-separated string naming the account. +// The AMOUNT follows the form: +// [COMM][WS]QUANTITY[WS][COMM][[WS]@[WS][COMM]PRICE[COMM]] +// For example: +// 200 AAPL @ $40.00 +// $50.00 +// DM 12.54 +// DM 12.54 @ $1.20 +// The NOTE can be anything. +// +// All entries must balance to 0.0, in every commodity. This means +// that a transaction with mixed commodities must balance by +// converting one of those commodities to the other. As a +// convenience, this is done automatically for you in the case where +// exactly two commodities are referred to, in which case the second +// commodity is converted into the first by computing which the price +// must have been in order to balance the transaction. Example: +// +// 2004.06.18 c (BUY) Apple Computer +// Assets:Brokerage $-200.00 +// Assets:Brokerage 100 AAPL +// +// What this transaction says is that $200 was paid from the +// brokerage account to buy 100 shares of Apple stock, and then place +// those same shares back in the brokerage account. From this point +// forward, the account "Assets:Brokerage" will have two balance +// totals: The number of dollars in the account, and the number of +// apple shares. +// In terms of the transaction, however, it must balance to zero, +// otherwise it would mean that something had been lost without +// accouting for it. So in this case what ledger will do is divide +// 100 by $200, to arrive at a per-share price of $2 for the APPL +// stock, and it will read this transaction as if it had been +// written: +// +// 2004.06.18 c (BUY) Apple Computer +// Assets:Brokerage $-200 +// Assets:Brokerage 100 AAPL @ $2 +// +// If you then wanted to give some of the shares to someone, in +// exchange for services rendered, use the regular single-commodity +// form of transaction: +// +// 2004.07.11 c A kick-back for the broker +// Assets:Brokerage -10 AAPL +// Expenses:Broker's Fees 10 AAPL +// +// This transaction does not need to know the price of AAPL on the +// given day, because none of the shares are being converted to +// another commodity. It simply directly affects the total number of +// AAPL shares held in "Assets:Brokerage". + +struct commodity +{ + std::string name; + std::string symbol; + + bool prefix; + bool separate; + + int precision; + + commodity() : prefix(false), separate(true) {} + commodity(const std::string& sym, bool pre, bool sep, int prec) + : symbol(sym), prefix(pre), separate(sep), precision(prec) {} +}; + +typedef std::map<const std::string, commodity *> commodities_t; +typedef commodities_t::iterator commodities_iterator; +typedef std::pair<const std::string, commodity *> commodities_entry; + +extern commodities_t commodities; +extern commodity * commodity_usd; + +class amount +{ + public: + virtual ~amount() {} + + virtual const std::string& comm_symbol() const = 0; + virtual amount * copy() const = 0; + virtual amount * value() const = 0; + + // Test if non-zero + + virtual operator bool() const = 0; + + // Assignment + + virtual void credit(const amount * other) = 0; + virtual void operator+=(const amount& other) = 0; + + // String conversion routines + + virtual void parse(const char * num) = 0; + virtual amount& operator=(const char * num) = 0; + virtual operator std::string() const = 0; +}; + +template<class Traits> +std::basic_ostream<char, Traits> & +operator<<(std::basic_ostream<char, Traits>& out, const amount& a) { + return (out << std::string(a)); +} + +extern amount * create_amount(const char * value, const amount * price = NULL); + +struct account; +struct transaction +{ + account * acct; + amount * cost; + + std::string note; + + transaction() : acct(NULL), cost(NULL) {} + + ~transaction() { + if (cost) + delete cost; + } +}; + +struct entry +{ + std::time_t date; + std::string code; + std::string desc; + + bool cleared; + + std::list<transaction *> xacts; + + entry() : cleared(false) {} + ~entry() { + for (std::list<transaction *>::iterator i = xacts.begin(); + i != xacts.end(); + i++) { + delete *i; + } + } + + void print(std::ostream& out) const; + bool validate() const; +}; + +struct cmp_entry_date { + bool operator()(const entry * left, const entry * right) { + return std::difftime(left->date, right->date) < 0; + } +}; + +class totals +{ + typedef std::map<const std::string, amount *> map_t; + typedef map_t::iterator iterator_t; + typedef map_t::const_iterator const_iterator_t; + typedef std::pair<const std::string, amount *> pair_t; + + map_t amounts; + + public: + void credit(const amount * val) { + std::pair<iterator_t, bool> result = + amounts.insert(pair_t(val->comm_symbol(), val->copy())); + if (! result.second) + amounts[val->comm_symbol()]->credit(val); + } + void credit(const totals& other); + + operator bool() const; + + void print(std::ostream& out) const; + + // Returns an allocated entity + amount * value(const std::string& comm); + amount * sum(const std::string& comm) { + return amounts[comm]; + } +}; + +struct account +{ + std::string name; + commodity * comm; // default commodity for this account + + struct account * parent; + + typedef std::map<const std::string, struct account *> map; + typedef map::iterator iterator; + typedef map::const_iterator const_iterator; + typedef std::pair<const std::string, struct account *> pair; + + map children; + + // Balance totals, by commodity + totals future; + totals current; + totals cleared; + + account(const std::string& _name, struct account * _parent = NULL) + : name(_name), parent(_parent) {} + + void credit(const entry * ent, const amount * amt) { + for (account * acct = this; acct; acct = acct->parent) { + acct->future.credit(amt); + + if (difftime(ent->date, std::time(NULL)) < 0) + acct->current.credit(amt); + + if (ent->cleared) + acct->cleared.credit(amt); + } + } + + operator std::string() const { + if (! parent) + return name; + else + return std::string(*parent) + ":" + name; + } +}; + +template<class Traits> +std::basic_ostream<char, Traits> & +operator<<(std::basic_ostream<char, Traits>& out, const account& a) { + return (out << std::string(a)); +} + + +typedef std::map<const std::string, account *> accounts_t; +typedef accounts_t::iterator accounts_iterator; +typedef std::pair<const std::string, account *> accounts_entry; + +extern accounts_t accounts; + +} // namespace ledger + +#endif // _LEDGER_H diff --git a/main.cc b/main.cc new file mode 100644 index 00000000..34cea2b0 --- /dev/null +++ b/main.cc @@ -0,0 +1,86 @@ +#include <fstream> +#include <vector> +#include <cassert> + +#include <pcre.h> // Perl regular expression library + +#include "ledger.h" + +////////////////////////////////////////////////////////////////////// +// +// Command-line parser and top-level logic. +// + +namespace ledger { + extern bool parse_ledger(std::istream& in, std::vector<entry *>& ledger); + extern bool parse_gnucash(std::istream& in, std::vector<entry *>& ledger); + extern void report_balances(std::ostream& out, std::vector<entry *>& ledger, + bool show_children, bool show_empty); + extern void print_ledger(std::ostream& out, std::vector<entry *>& ledger); +} + +using namespace ledger; + +int main(int argc, char *argv[]) +{ + // Setup global defaults + + commodity_usd = new commodity("$", true, false, 2); + commodities.insert(commodities_entry("$", commodity_usd)); + commodities.insert(commodities_entry("USD", commodity_usd)); + + // Parse the command-line options + + bool show_children = false; + bool show_empty = false; + + int c; + while (-1 != (c = getopt(argc, argv, "sS"))) { + switch (char(c)) { + case 's': show_children = true; break; + case 'S': show_empty = true; break; + } + } + + if (optind == argc) { + std::cerr << "usage: ledger [options] DATA_FILE COMMAND [ARGS]" + << std::endl + << "options:" << std::endl + << " -s show sub-accounts in balance totals" << std::endl + << " -S show empty accounts in balance totals" << std::endl + << "commands:" << std::endl + << " balance show balance totals" << std::endl + << " print print all ledger entries" << std::endl; + std::exit(1); + } + + // Parse the ledger + + std::ifstream file(argv[optind++]); + std::vector<entry *> ledger; + + char buf[256]; + file.get(buf, 255); + file.seekg(0); + + if (std::strncmp(buf, "<?xml version=\"1.0\"?>", 21) == 0) + parse_gnucash(file, ledger); + else + parse_ledger(file, ledger); + + // Read the command word + + if (optind == argc) { + std::cerr << "Command word missing" << std::endl; + return 1; + } + + const std::string command = argv[optind++]; + + // Process the command + + if (command == "balance") + report_balances(std::cout, ledger, show_children, show_empty); + else if (command == "print") + print_ledger(std::cout, ledger); +} diff --git a/parse.cc b/parse.cc new file mode 100644 index 00000000..3f18b8f6 --- /dev/null +++ b/parse.cc @@ -0,0 +1,189 @@ +#include <iostream> +#include <vector> +#include <cstring> +#include <ctime> +#include <cctype> +#include <cassert> + +#include <pcre.h> // Perl regular expression library + +#include "ledger.h" + +namespace ledger { + +////////////////////////////////////////////////////////////////////// +// +// Ledger parser +// + +char * next_element(char * buf, bool variable = false) +{ + char * p; + + if (variable) + p = std::strstr(buf, " "); + else + p = std::strchr(buf, ' '); + + if (! p) + return NULL; + + *p++ = '\0'; + while (std::isspace(*p)) + p++; + + return p; +} + +static int linenum = 0; + +void finalize_entry(entry * curr, std::vector<entry *>& ledger) +{ + if (curr) { + if (! curr->validate()) { + std::cerr << "Failed to balance the following transaction, " + << "ending on line " << (linenum - 1) << std::endl; + curr->print(std::cerr); + } else { + ledger.push_back(curr); + } + } +} + +bool parse_ledger(std::istream& in, std::vector<entry *>& ledger) +{ + static std::time_t now = std::time(NULL); + static struct std::tm * now_tm = std::localtime(&now); + static int current_year = now_tm->tm_year + 1900; + + static char line[1024]; + + static struct std::tm moment; + memset(&moment, 0, sizeof(struct std::tm)); + + entry * curr = NULL; + + // Compile the regular expression used for parsing amounts + static pcre * entry_re = NULL; + if (! entry_re) { + const char *error; + int erroffset; + static const std::string regexp = + "^(([0-9]{4})[./])?([0-9]{2})[./]([0-9]{2})\\s+(\\*\\s+)?" + "(\\(([^)]+)\\)\\s+)?(.+)"; + entry_re = pcre_compile(regexp.c_str(), 0, &error, &erroffset, NULL); + } + + while (! in.eof()) { + in.getline(line, 1023); + linenum++; + + if (in.eof()) { + break; + } + else if (std::isdigit(line[0])) { + static char buf[256]; + int ovector[60]; + + int matched = pcre_exec(entry_re, NULL, line, std::strlen(line), + 0, 0, ovector, 60); + if (! matched) { + std::cerr << "Failed to parse, line " << linenum << ": " + << line << std::endl; + continue; + } + + if (curr) + finalize_entry(curr, ledger); + curr = new entry; + + // Parse the date + + int mday, mon, year = current_year; + + if (ovector[1 * 2] >= 0) { + pcre_copy_substring(line, ovector, matched, 2, buf, 255); + year = std::atoi(buf); + } + + if (ovector[3 * 2] >= 0) { + pcre_copy_substring(line, ovector, matched, 3, buf, 255); + mon = std::atoi(buf); + } + + if (ovector[4 * 2] >= 0) { + pcre_copy_substring(line, ovector, matched, 4, buf, 255); + mday = std::atoi(buf); + } + + moment.tm_mday = mday; + moment.tm_mon = mon - 1; + moment.tm_year = year - 1900; + + curr->date = std::mktime(&moment); + + if (ovector[5 * 2] >= 0) + curr->cleared = true; + + if (ovector[6 * 2] >= 0) { + pcre_copy_substring(line, ovector, matched, 7, buf, 255); + curr->code = buf; + } + + if (ovector[8 * 2] >= 0) { + int result = pcre_copy_substring(line, ovector, matched, 8, buf, 255); + assert(result >= 0); + curr->desc = buf; + } + } + else if (std::isspace(line[0])) { + transaction * xact = new transaction(); + + xact->cost = create_amount(next_element(line, true)); + + // jww (2003-09-28): Reverse parse the account name to find the + // correct account. This means that each account needs to know + // its children. + account * current = NULL; + for (char * tok = std::strtok(line, ":"); + tok; + tok = std::strtok(NULL, ":")) { + if (! current) { + accounts_iterator i = accounts.find(tok); + if (i == accounts.end()) { + current = new account(tok); + accounts.insert(accounts_entry(tok, current)); + } else { + current = (*i).second; + } + } else { + account::iterator i = current->children.find(tok); + if (i == current->children.end()) { + current = new account(tok, current); + current->parent->children.insert(accounts_entry(tok, current)); + } else { + current = (*i).second; + } + } + + // Apply transaction to account (and all parent accounts) + + assert(current); + current->credit(curr, xact->cost); + } + xact->acct = current; + + curr->xacts.push_back(xact); + } + else if (line[0] == 'Y') { + current_year = std::atoi(line + 2); + } + } + + if (curr) + finalize_entry(curr, ledger); + + return true; +} + +} // namespace ledger |