diff options
-rw-r--r-- | Makefile | 28 | ||||
-rw-r--r-- | amount.cc | 129 | ||||
-rw-r--r-- | balance.cc | 27 | ||||
-rw-r--r-- | gnucash.cc | 9 | ||||
-rw-r--r-- | ledger.cc | 63 | ||||
-rw-r--r-- | ledger.h | 57 | ||||
-rw-r--r-- | main.cc | 254 | ||||
-rw-r--r-- | parse.cc | 170 | ||||
-rw-r--r-- | register.cc | 85 | ||||
-rwxr-xr-x | report | 4 |
10 files changed, 564 insertions, 262 deletions
@@ -1,3 +1,14 @@ +define GNUCASH +true +endef +define HUQUQ +true +endef + +# +# Example build: make GNUCASH=1 COMMODITY=EUR +# + CODE = amount.cc \ ledger.cc \ parse.cc \ @@ -8,16 +19,23 @@ CODE = amount.cc \ OBJS = $(patsubst %.cc,%.o,$(CODE)) -CFLAGS = -Wall -ansi -pedantic -DDEFAULT_COMMODITY="\"\$$\"" -CFLAGS := $(CFLAGS) -DHUQUQULLAH=1 +ifndef COMMODITY +COMMODITY = \$$ +endif -DFLAGS = -O3 -fomit-frame-pointer -#DFLAGS = -g -O2 # -pg +CFLAGS = -Wall -ansi -pedantic +CFLAGS := $(CFLAGS) -DDEFAULT_COMMODITY="\"$(COMMODITY)\"" -INCS = -I/usr/include/xmltok +#DFLAGS = -O3 -fomit-frame-pointer +DFLAGS = -g # -O2 # -pg +INCS = -I/usr/include/xmltok LIBS = -lgmpxx -lgmp -lpcre +ifdef HUQUQ +CFLAGS := $(CFLAGS) -DHUQUQULLAH=1 +endif + ifdef GNUCASH CODE := $(CODE) gnucash.cc CFLAGS := $(CFLAGS) -DREAD_GNUCASH=1 @@ -52,6 +52,10 @@ class gmp_amount : public amount virtual amount * copy() const; virtual amount * value(amount *) const; virtual amount * street() const; + virtual bool has_price() const { + return priced; + } + virtual void set_value(const amount * val); virtual operator bool() const; @@ -72,56 +76,15 @@ class gmp_amount : public amount return as_str(false); } - friend amount * create_amount(const char * value, const amount * price); + friend amount * create_amount(const char * value, const amount * cost); }; -amount * create_amount(const char * value, const amount * price) +amount * create_amount(const char * value, const amount * cost) { 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); - } + if (cost) + a->set_value(cost); return a; } @@ -137,15 +100,28 @@ static void round(mpz_t out, const mpz_t val, int prec) 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); + + if (mpz_sgn(remainder) < 0) { + mpz_ui_sub(divisor, 0, divisor); + + if (mpz_cmp(remainder, divisor) < 0) { + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); + mpz_add(remainder, divisor, remainder); + mpz_ui_sub(remainder, 0, remainder); + mpz_add(out, val, remainder); + } else { + mpz_sub(out, val, remainder); + } } else { - mpz_sub(out, val, remainder); + 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); @@ -204,6 +180,10 @@ amount * gmp_amount::value(amount * pr) const else new_amt->quantity_comm = quantity_comm; + if (new_amt->quantity_comm->precision < MAX_PRECISION) + round(new_amt->quantity, new_amt->quantity, + new_amt->quantity_comm->precision); + return new_amt; } else if (! priced) { @@ -211,8 +191,14 @@ amount * gmp_amount::value(amount * pr) const } else { gmp_amount * new_amt = new gmp_amount(); + multiply(new_amt->quantity, quantity, price); + new_amt->quantity_comm = price_comm; + if (new_amt->quantity_comm->precision < MAX_PRECISION) + round(new_amt->quantity, new_amt->quantity, + new_amt->quantity_comm->precision); + return new_amt; } } @@ -265,6 +251,39 @@ amount * gmp_amount::street() const return cost ? cost : copy(); } +void gmp_amount::set_value(const amount * val) +{ + assert(! priced); // don't specify the pricing twice! + + const gmp_amount * v = dynamic_cast<const gmp_amount *>(val); + assert(v); + + 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, v->quantity, quantity); + mpz_mul(remainder, remainder, addend); + mpz_tdiv_q(remainder, remainder, quantity); + mpz_mul(quotient, quotient, addend); + mpz_add(quotient, quotient, remainder); + mpz_abs(quotient, quotient); + + priced = true; + mpz_set(price, quotient); + price_comm = v->quantity_comm; + + mpz_clear(quotient); + mpz_clear(remainder); + mpz_clear(addend); +} + gmp_amount::operator bool() const { mpz_t copy; @@ -535,16 +554,10 @@ static commodity * parse_amount(mpz_t out, const char * num, thousands, european, precision); } else { comm = (*item).second; -#if 0 - // If a finer precision was used than the commodity allows, - // increase the precision. - if (precision > comm->precision) - comm->precision = precision; -#else + if (use_warnings && precision > comm->precision) std::cerr << "Warning: Use of higher precision than expected: " << value_str << std::endl; -#endif } } @@ -51,7 +51,7 @@ static void display_total(std::ostream& out, totals& balance, displayed = true; out << acct->balance; - if (top_level) + if (! no_subtotals && top_level) balance.credit(acct->balance); if (acct->parent && ! no_subtotals && ! full_names) { @@ -126,14 +126,23 @@ void report_balances(int argc, char **argv, std::ostream& out) for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); x != (*i)->xacts.end(); x++) { - account * acct = (*x)->acct; - - for (; acct; acct = no_subtotals ? NULL : acct->parent) { - bool true_match = false; - if (! (regexps.empty() || - account_matches(acct, regexps, &true_match))) + for (account * acct = (*x)->acct; + acct; + acct = no_subtotals ? NULL : acct->parent) { + if (acct->checked == 0) { + bool true_match = false; + if (! (regexps.empty() || + account_matches(acct, regexps, &true_match))) + acct->checked = 2; + else if (! (true_match || show_children || ! acct->parent)) + acct->checked = 3; + else + acct->checked = 1; + } + + if (acct->checked == 2) break; - else if (! (true_match || show_children || ! acct->parent)) + else if (acct->checked == 3) continue; acct->display = true; @@ -154,7 +163,7 @@ void report_balances(int argc, char **argv, std::ostream& out) // Print the total of all the balances shown - if (! no_subtotals) + if (! no_subtotals && balance) out << "--------------------" << std::endl << balance << std::endl; } @@ -204,8 +204,17 @@ static void dataHandler(void *userData, const char *s, int len) xact->acct = (*i).second; std::string value = curr_quant + " " + (*i).second->comm->symbol; + + if (curr_value->comm() == (*i).second->comm) { + // assert: value must be equal to curr_value. + delete curr_value; + curr_value = NULL; + } xact->cost = create_amount(value.c_str(), curr_value); + if (curr_value) + delete curr_value; + if (do_compute) xact->acct->balance.credit(xact->cost); break; @@ -10,12 +10,7 @@ state main_ledger; std::list<mask> regexps; -#ifdef HUQUQULLAH -bool compute_huquq; -std::list<mask> huquq_categories; -#endif - -void entry::print(std::ostream& out) const +void entry::print(std::ostream& out, bool shortcut) const { char buf[32]; std::strftime(buf, 31, "%Y.%m.%d ", std::localtime(&date)); @@ -30,46 +25,48 @@ void entry::print(std::ostream& out) const out << std::endl; - bool shortcut = xacts.size() == 2; if (shortcut && - xacts.front()->cost->comm() != xacts.back()->cost->comm()) + (xacts.size() != 2 || + xacts.front()->cost->comm() != xacts.back()->cost->comm())) { shortcut = false; + } - for (std::list<transaction *>::const_iterator i = xacts.begin(); - i != xacts.end(); - i++) { + for (std::list<transaction *>::const_iterator x = xacts.begin(); + x != xacts.end(); + x++) { out << " "; out.width(30); - out << std::left << (*i)->acct->as_str(); + out << std::left << (*x)->acct->as_str(); - if (! shortcut || i == xacts.begin()) { + if ((*x)->cost && (! shortcut || x == xacts.begin())) { out << " "; out.width(10); - out << std::right << (*i)->cost->as_str(true); + out << std::right << (*x)->cost->as_str(true); } - if (! (*i)->note.empty()) - out << " ; " << (*i)->note; + if (! (*x)->note.empty()) + out << " ; " << (*x)->note; out << std::endl; } out << std::endl; } -bool entry::validate() const +bool entry::validate(bool show_unaccounted) const { totals balance; - for (std::list<transaction *>::const_iterator i = xacts.begin(); - i != xacts.end(); - i++) - balance.credit((*i)->cost->value()); + for (std::list<transaction *>::const_iterator x = xacts.begin(); + x != xacts.end(); + x++) + if ((*x)->cost) + balance.credit((*x)->cost->value()); - if (balance) { - std::cerr << "Totals are:" << std::endl; + if (show_unaccounted && balance) { + std::cerr << "Unaccounted-for balances are:" << std::endl; balance.print(std::cerr, 20); - std::cerr << std::endl; + std::cerr << std::endl << std::endl; } return ! balance; // must balance to 0.0 } @@ -155,8 +152,17 @@ amount * totals::value(const std::string& commodity) const void print_ledger(int argc, char *argv[], std::ostream& out) { + bool use_shortcuts = true; + optind = 1; + int c; + while (-1 != (c = getopt(argc, argv, "n"))) { + switch (char(c)) { + case 'n': use_shortcuts = false; break; + } + } + // Compile the list of specified regular expressions, which can be // specified on the command line, or using an include/exclude file @@ -172,7 +178,7 @@ void print_ledger(int argc, char *argv[], std::ostream& out) i != main_ledger.entries.end(); i++) if ((*i)->matches(regexps)) - (*i)->print(out); + (*i)->print(out, use_shortcuts); } void record_regexp(char * pattern, std::list<mask>& regexps) @@ -317,9 +323,14 @@ account * state::find_account(const char * name, bool create) delete[] buf; - if (current) + if (current) { accounts_cache.insert(accounts_entry(name, current)); +#ifdef HUQUQULLAH + if (matches(main_ledger.huquq_categories, name)) + current->exempt_or_necessary = true; +#endif + } return current; } @@ -1,5 +1,5 @@ #ifndef _LEDGER_H -#define _LEDGER_H "$Revision: 1.12 $" +#define _LEDGER_H "$Revision: 1.13 $" ////////////////////////////////////////////////////////////////////// // @@ -15,7 +15,12 @@ #include <list> #include <map> #include <ctime> + +#ifdef DEBUG #include <cassert> +#else +#define assert(x) +#endif #include <pcre.h> // Perl regular expression library @@ -119,6 +124,9 @@ class amount virtual amount * value(amount * pr = NULL) const = 0; virtual amount * street() const = 0; + virtual bool has_price() const = 0; + virtual void set_value(const amount * pr) = 0; + // Test if non-zero virtual operator bool() const = 0; @@ -143,8 +151,8 @@ 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); - +extern amount * create_amount(const char * value, + const amount * cost = NULL); struct mask { @@ -169,8 +177,16 @@ struct transaction amount * cost; std::string note; +#ifdef HUQUQULLAH + bool exempt_or_necessary; +#endif - transaction() : acct(NULL), cost(NULL) {} + transaction(account * _acct = NULL, amount * _cost = NULL) + : acct(_acct), cost(_cost) { +#ifdef HUQUQULLAH + exempt_or_necessary = false; +#endif + } #ifdef DO_CLEANUP ~transaction() { @@ -207,8 +223,8 @@ struct entry #endif bool matches(const std::list<mask>& regexps) const; - void print(std::ostream& out) const; - bool validate() const; + void print(std::ostream& out, bool shortcut = true) const; + bool validate(bool show_unaccounted = false) const; }; struct cmp_entry_date { @@ -268,7 +284,12 @@ struct account std::string name; commodity * comm; // default commodity for this account totals balance; - bool display; + + bool display; + int checked; +#ifdef HUQUQULLAH + bool exempt_or_necessary; +#endif typedef std::map<const std::string, struct account *> map; typedef map::iterator iterator; @@ -278,7 +299,11 @@ struct account map children; account(const std::string& _name, struct account * _parent = NULL) - : parent(_parent), name(_name), display(false) {} + : parent(_parent), name(_name), display(false), checked(0) { +#ifdef HUQUQULLAH + exempt_or_necessary = false; +#endif + } const std::string as_str() const { if (! parent) @@ -300,11 +325,6 @@ typedef accounts_t::iterator accounts_iterator; typedef std::pair<const std::string, account *> accounts_entry; -#ifdef HUQUQULLAH -extern bool compute_huquq; -extern std::list<mask> huquq_categories; -#endif - struct state { commodities_t commodities; @@ -313,6 +333,17 @@ struct state entries_t entries; totals prices; +#ifdef HUQUQULLAH + bool compute_huquq; + std::list<mask> huquq_categories; + amount * huquq; + commodity * huquq_commodity; + account * huquq_account; + account * huquq_expenses_account; + + state() : compute_huquq(false) {} +#endif + #ifdef DO_CLEANUP ~state(); #endif @@ -24,16 +24,85 @@ namespace ledger { using namespace ledger; -void show_help(std::ostream& out) +static void show_help(std::ostream& out) { - out << "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::cerr + << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl + << std::endl + << "ledger options:" << std::endl + << " -C also show cleared transactions" << std::endl + << " -b DATE specify a beginning date" << std::endl + << " -c do not show future entries (same as -e TODAY)" << std::endl + << " -e DATE specify an ending date" << std::endl + << " -f FILE specify pathname of ledger data file" << std::endl + << " -h display this help text" << std::endl +#ifdef HUQUQULLAH + << " -H do not auto-compute Huququ'llah" << std::endl +#endif + << " -i FILE read the list of inclusion regexps from FILE" << std::endl + << " -p FILE read the list of prices from FILE" << std::endl + << " -P download price quotes from the Internet" << std::endl + << " (this works by running the command \"getquote SYMBOL\")" + << std::endl + << " -w print out warnings where applicable" << std::endl + << std::endl + << "commands:" << std::endl + << " balance show balance totals" << std::endl + << " register display a register for ACCOUNT" << std::endl + << " print print all ledger entries" << std::endl + << " equity generate equity ledger for all entries" << std::endl + << std::endl + << "`balance' options:" << std::endl + << " -F print each account's full name" << std::endl + << " -n do not generate totals for parent accounts" << std::endl + << " -s show sub-accounts in balance totals" << std::endl + << " -S show empty accounts in balance totals" << std::endl; +} + +static const char *formats[] = { + "%Y/%m/%d", + "%m/%d", + "%Y.%m.%d", + "%m.%d", + "%a", + "%A", + "%b", + "%B", + "%Y", + NULL +}; + +static bool parse_date(const char * date_str, std::time_t * result) +{ + struct std::tm when; + + std::time_t now = std::time(NULL); + struct std::tm * now_tm = std::localtime(&now); + + for (const char ** f = formats; *f; f++) { + memset(&when, INT_MAX, sizeof(struct std::tm)); + if (strptime(date_str, *f, &when)) { + when.tm_hour = 0; + when.tm_min = 0; + when.tm_sec = 0; + + if (when.tm_year == -1) + when.tm_year = now_tm->tm_year; + + if (std::strcmp(*f, "%Y") == 0) { + when.tm_mon = 0; + when.tm_mday = 1; + } else { + if (when.tm_mon == -1) + when.tm_mon = now_tm->tm_mon; + if (when.tm_mday == -1) + when.tm_mday = now_tm->tm_mday; + } + *result = std::mktime(&when); + return true; + } + } + return false; } ////////////////////////////////////////////////////////////////////// @@ -53,37 +122,81 @@ int main(int argc, char *argv[]) std::istream * file = NULL; #ifdef HUQUQULLAH - compute_huquq = true; + bool compute_huquq = true; #endif have_beginning = false; have_ending = false; show_cleared = false; int c; - while (-1 != (c = getopt(argc, argv, "+b:e:cChHwf:i:p:P"))) { + while (-1 != (c = getopt(argc, argv, "+b:e:d:cChHwf:i:p:P"))) { switch (char(c)) { - case 'b': { - struct tm * when = getdate(optarg); - if (! when) { - std::cerr << "Error: Bad begin date string: " << optarg - << std::endl; - } else { - begin_date = std::mktime(when); + case 'b': + case 'e': { + std::time_t when; + if (! parse_date(optarg, &when)) { + std::cerr << "Error: Bad date string: " << optarg << std::endl; + return 1; + } + + if (c == 'b') { + begin_date = when; have_beginning = true; + } else { + end_date = when; + have_ending = true; } break; } - case 'e': { - struct tm * when = getdate(optarg); - if (! when) { - std::cerr << "Error: Bad end date string: " << optarg - << std::endl; - } else { - end_date = std::mktime(when); - have_ending = true; + + case 'd': { + if (! parse_date(optarg, &begin_date)) { + std::cerr << "Error: Bad date string: " << optarg << std::endl; + return 1; + } + have_beginning = true; + + struct std::tm when, then; + std::memset(&then, 0, sizeof(struct std::tm)); + + std::time_t now = std::time(NULL); + struct std::tm * now_tm = std::localtime(&now); + + for (const char ** f = formats; *f; f++) { + memset(&when, INT_MAX, sizeof(struct std::tm)); + if (strptime(optarg, *f, &when)) { + then.tm_hour = 0; + then.tm_min = 0; + then.tm_sec = 0; + + if (when.tm_year != -1) + then.tm_year = when.tm_year + 1; + else + then.tm_year = now_tm->tm_year; + + if (std::strcmp(*f, "%Y") == 0) { + then.tm_mon = 0; + then.tm_mday = 1; + } else { + if (when.tm_mon != -1) + then.tm_mon = when.tm_mon + 1; + else + then.tm_mon = now_tm->tm_mon; + + if (when.tm_mday != -1) + then.tm_mday = when.tm_mday + 1; + else + then.tm_mday = now_tm->tm_mday; + } + + end_date = std::mktime(&then); + have_ending = true; + break; + } } break; } + case 'c': end_date = std::time(NULL); have_ending = true; @@ -127,46 +240,40 @@ int main(int argc, char *argv[]) } if (optind == argc) { - std::cerr - << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl - << std::endl - << "ledger options:" << std::endl - << " -C also show cleared transactions" << std::endl - << " -b DATE specify a beginning date" << std::endl - << " -c do not show future entries (same as -e TODAY)" << std::endl - << " -e DATE specify an ending date" << std::endl - << " -f FILE specify pathname of ledger data file" << std::endl - << " -h display this help text" << std::endl -#ifdef HUQUQULLAH - << " -H do not auto-compute Huququ'llah" << std::endl -#endif - << " -i FILE read the list of inclusion regexps from FILE" << std::endl - << " -p FILE read the list of prices from FILE" << std::endl - << " -P download price quotes from the Internet" << std::endl - << " (this works by running the command \"getquote SYMBOL\")" - << std::endl - << " -w print out warnings where applicable" << std::endl - << std::endl - << "commands:" << std::endl - << " balance show balance totals" << std::endl - << " register display a register for ACCOUNT" << std::endl - << " print print all ledger entries" << std::endl - << " equity generate equity ledger for all entries" << std::endl - << std::endl - << "`balance' options:" << std::endl - << " -F print each account's full name" << std::endl - << " -n do not generate totals for parent accounts" << std::endl - << " -s show sub-accounts in balance totals" << std::endl - << " -S show empty accounts in balance totals" << std::endl; + show_help(std::cout); return 1; } - // The -f option is required + if (use_warnings && (have_beginning || have_ending)) { + std::cout << "Reporting"; - if (! file || ! *file) { - std::cerr << "Please specify the ledger file using the -f option." - << std::endl; - return 1; + if (have_beginning) { + char buf[32]; + std::strftime(buf, 31, "%Y.%m.%d", std::localtime(&begin_date)); + std::cout << " from " << buf; + } + + if (have_ending) { + char buf[32]; + std::strftime(buf, 31, "%Y.%m.%d", std::localtime(&end_date)); + std::cout << " until " << buf; + } + + std::cout << std::endl; + } + + // A ledger data file must be specified + + if (! file) { + const char * p = std::getenv("LEDGER"); + if (p) + file = new std::ifstream(p); + + if (! file || ! *file) { + std::cerr << "Please specify the ledger file using the -f option." + << std::endl; + return 1; + } } // Read the command word @@ -174,17 +281,34 @@ int main(int argc, char *argv[]) const std::string command = argv[optind]; #ifdef HUQUQULLAH - if (command == "register") + if (command == "register" && argv[optind + 1] && + std::string(argv[optind + 1]) != "Huququ'llah" && + std::string(argv[optind + 1]) != "Expenses:Huququ'llah") compute_huquq = false; if (compute_huquq) { - new commodity("H", true, true, true, false, 2); + main_ledger.compute_huquq = true; + main_ledger.huquq_commodity = new commodity("H", true, true, + true, false, 2); + + // The allocation causes it to be inserted into the + // main_ledger.commodities mapping. new commodity("mithqal", false, true, true, false, 1); - read_regexps(".huquq", huquq_categories); + read_regexps(".huquq", main_ledger.huquq_categories); main_ledger.record_price("H=" DEFAULT_COMMODITY "0.19"); + + bool save_use_warnings = use_warnings; + use_warnings = false; main_ledger.record_price("troy=8.5410148523 mithqal"); + use_warnings = save_use_warnings; + + main_ledger.huquq = create_amount("H 1.00"); + + main_ledger.huquq_account = main_ledger.find_account("Huququ'llah"); + main_ledger.huquq_expenses_account = + main_ledger.find_account("Expenses:Huququ'llah"); } #endif @@ -32,17 +32,101 @@ static char * next_element(char * buf, bool variable = false) static int linenum = 0; -static inline void finalize_entry(entry * curr) +static void finalize_entry(entry * curr, bool compute_balances) { - 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 { - main_ledger.entries.push_back(curr); + assert(curr); + + // Certain shorcuts are allowed in the case of exactly two + // transactions. + + if (! curr->xacts.empty() && curr->xacts.size() == 2) { + transaction * first = curr->xacts.front(); + transaction * second = curr->xacts.back(); + + // If one transaction gives no value at all, then its value is + // the inverse of the computed value of the other. + + if (! first->cost && second->cost) { + first->cost = second->cost->value(); + first->cost->negate(); + + if (compute_balances) + first->acct->balance.credit(first->cost); + } + else if (! second->cost && first->cost) { + second->cost = first->cost->value(); + second->cost->negate(); + + if (compute_balances) + second->acct->balance.credit(second->cost); + } + else if (first->cost && second->cost) { + // If one transaction is of a different commodity than the + // other, and it has no per-unit price, and its not of the + // default commodity, then determine its price by dividing the + // unit count into the total, to balance the transaction. + + if (first->cost->comm() != second->cost->comm()) { + if (! second->cost->has_price() && + second->cost->comm_symbol() != DEFAULT_COMMODITY) { + second->cost->set_value(first->cost); + } + else if (! first->cost->has_price() && + first->cost->comm_symbol() != DEFAULT_COMMODITY) { + first->cost->set_value(second->cost); + } + } } } + + if (! curr->validate()) { + std::cerr << "Error, line " << (linenum - 1) + << ": Failed to balance the following transaction:" + << std::endl; + curr->print(std::cerr); + curr->validate(true); + return; + } + +#ifdef HUQUQULLAH + if (main_ledger.compute_huquq) { + for (std::list<transaction *>::iterator x = curr->xacts.begin(); + x != curr->xacts.end(); + x++) { + if (! (*x)->exempt_or_necessary || ! (*x)->cost) + continue; + + // Reflect the exempt or necessary transaction in the + // Huququ'llah account, using the H commodity, which is 19% of + // whichever DEFAULT_COMMODITY ledger was compiled with. + + amount * temp = (*x)->cost->value(); + + transaction * t + = new transaction(main_ledger.huquq_account, + temp->value(main_ledger.huquq)); + curr->xacts.push_back(t); + + if (compute_balances) + t->acct->balance.credit(t->cost); + + // Balance the above transaction by recording the inverse in + // Expenses:Huququ'llah. + + t = new transaction(main_ledger.huquq_expenses_account, + temp->value(main_ledger.huquq)); + t->cost->negate(); + curr->xacts.push_back(t); + + if (compute_balances) + t->acct->balance.credit(t->cost); + + delete temp; + } + } +#endif + + main_ledger.entries.push_back(curr); } ////////////////////////////////////////////////////////////////////// @@ -59,7 +143,6 @@ bool parse_ledger(std::istream& in, bool compute_balances) char line[1024]; struct std::tm moment; - memset(&moment, 0, sizeof(struct std::tm)); entry * curr = NULL; @@ -86,15 +169,15 @@ bool parse_ledger(std::istream& in, bool compute_balances) 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; + std::cerr << "Error, line " << linenum + << ": Failed to parse: " << line << std::endl; continue; } // If we haven't finished with the last entry yet, do so now if (curr) - finalize_entry(curr); + finalize_entry(curr, compute_balances); curr = new entry; @@ -114,6 +197,8 @@ bool parse_ledger(std::istream& in, bool compute_balances) pcre_copy_substring(line, ovector, matched, 4, buf, 255); int mday = std::atoi(buf); + memset(&moment, 0, sizeof(struct std::tm)); + moment.tm_mday = mday; moment.tm_mon = mon - 1; moment.tm_year = year - 1900; @@ -131,12 +216,11 @@ bool parse_ledger(std::istream& in, bool compute_balances) } if (ovector[8 * 2] >= 0) { - int result = pcre_copy_substring(line, ovector, matched, 8, buf, 255); - assert(result >= 0); + pcre_copy_substring(line, ovector, matched, 8, buf, 255); curr->desc = buf; } } - else if (std::isspace(line[0])) { + else if (curr && std::isspace(line[0])) { transaction * xact = new transaction(); char * p = line; @@ -162,8 +246,7 @@ bool parse_ledger(std::istream& in, bool compute_balances) xact->note = cost_str; } - xact->cost = curr->xacts.front()->cost->copy(); - xact->cost->negate(); + xact->cost = NULL; } else { note_str = std::strchr(cost_str, ';'); @@ -183,56 +266,21 @@ bool parse_ledger(std::istream& in, bool compute_balances) } #ifdef HUQUQULLAH - bool exempt_or_necessary = false; - if (compute_huquq) { - if (*p == '!') { - exempt_or_necessary = true; - p++; - } - else if (matches(huquq_categories, p)) { - exempt_or_necessary = true; - } + if (*p == '!') { + xact->exempt_or_necessary = true; + p++; } #endif xact->acct = main_ledger.find_account(p); - if (compute_balances) +#ifdef HUQUQULLAH + if (xact->acct->exempt_or_necessary) + xact->exempt_or_necessary = true; +#endif + if (compute_balances && xact->cost) xact->acct->balance.credit(xact->cost); curr->xacts.push_back(xact); - -#ifdef HUQUQULLAH - if (exempt_or_necessary) { - static amount * huquq = create_amount("H 1.00"); - amount * temp; - - // Reflect the exempt or necessary transaction in the - // Huququ'llah account, using the H commodity, which is 19% - // of whichever DEFAULT_COMMODITY ledger was compiled with. - transaction * t = new transaction(); - t->acct = main_ledger.find_account("Huququ'llah"); - temp = xact->cost->value(); - t->cost = temp->value(huquq); - delete temp; - curr->xacts.push_back(t); - - if (compute_balances) - t->acct->balance.credit(t->cost); - - // Balance the above transaction by recording the inverse in - // Expenses:Huququ'llah. - t = new transaction(); - t->acct = main_ledger.find_account("Expenses:Huququ'llah"); - temp = xact->cost->value(); - t->cost = temp->value(huquq); - delete temp; - t->cost->negate(); - curr->xacts.push_back(t); - - if (compute_balances) - t->acct->balance.credit(t->cost); - } -#endif } else if (line[0] == 'Y') { current_year = std::atoi(line + 2); @@ -240,7 +288,7 @@ bool parse_ledger(std::istream& in, bool compute_balances) } if (curr) - finalize_entry(curr); + finalize_entry(curr, compute_balances); return true; } diff --git a/register.cc b/register.cc index 5b5ea1dd..e0128d10 100644 --- a/register.cc +++ b/register.cc @@ -11,6 +11,18 @@ extern bool have_beginning; extern std::time_t end_date; extern bool have_ending; +static std::string truncated(const std::string& str, int width) +{ + char buf[256]; + memset(buf, '\0', 255); + std::strncpy(buf, str.c_str(), width); + if (buf[width - 1]) + std::strcpy(&buf[width - 3], "..."); + else + buf[width] = '\0'; + return buf; +} + ////////////////////////////////////////////////////////////////////// // // Register printing code @@ -19,16 +31,15 @@ extern bool have_ending; void print_register(int argc, char **argv, std::ostream& out) { optind = 1; -#if 0 - int c; - while (-1 != (c = getopt(argc, argv, ""))) { - switch (char(c)) { - } - } -#endif // Find out which account this register is to be printed for + if (optind == argc) { + std::cerr << ("Error: Must specify an account name " + "after the 'register' command.") << std::endl; + return; + } + account * acct = main_ledger.find_account(argv[optind++], false); if (! acct) { std::cerr << "Error: Unknown account name: " << argv[optind - 1] @@ -50,17 +61,7 @@ void print_register(int argc, char **argv, std::ostream& out) for (entries_iterator i = main_ledger.entries.begin(); i != main_ledger.entries.end(); i++) { - bool applies = false; - for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); - x != (*i)->xacts.end(); - x++) { - if ((*x)->acct == acct) { - applies = true; - break; - } - } - - if (! applies || ! (*i)->matches(regexps)) + if (! (*i)->matches(regexps)) continue; for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); @@ -91,21 +92,59 @@ void print_register(int argc, char **argv, std::ostream& out) if ((*i)->desc.empty()) out << " "; else - out << std::left << (*i)->desc; + out << std::left << truncated((*i)->desc, 30); out << " "; - transaction * xact = (*i)->xacts.front(); + // Always display the street value, if prices have been + // specified + + amount * street = (*x)->cost->street(); + balance.credit(street); + + // If there are two transactions, use the one which does not + // refer to this account. If there are more than two, we will + // just have to print all of the splits, like gnucash does. + + transaction * xact; + if ((*i)->xacts.size() == 2) { + if (*x == (*i)->xacts.front()) + xact = (*i)->xacts.back(); + else + xact = (*i)->xacts.front(); + } else { + xact = *x; + } out.width(22); - out << std::left << xact->acct->as_str() << " "; + out << std::left << truncated(xact->acct->as_str(), 22) << " "; out.width(12); - out << std::right << (*x)->cost->as_str(true); + out << std::right << street->as_str(true); + delete street; - balance.credit((*x)->cost); balance.print(out, 12); out << std::endl; + + if (xact != *x) + continue; + + for (std::list<transaction *>::iterator y = (*i)->xacts.begin(); + y != (*i)->xacts.end(); + y++) { + if (*x == *y) + continue; + + out << " "; + + out.width(22); + out << std::left << truncated((*y)->acct->as_str(), 22) << " "; + + out.width(12); + street = (*y)->cost->street(); + out << std::right << street->as_str(true) << std::endl; + delete street; + } } } } @@ -1,7 +1,7 @@ #!/bin/bash -binary=./ledger -ledger=ledger.dat +binary=ledger +LEDGER=$HOME/doc/finance/ledger.dat line="$binary -f $ledger" |