summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile47
-rw-r--r--amount.cc473
-rw-r--r--balance.cc123
-rw-r--r--gnucash.cc251
-rw-r--r--ledger.cc124
-rw-r--r--ledger.h270
-rw-r--r--main.cc86
-rw-r--r--parse.cc189
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