diff options
author | John Wiegley <johnw@newartisans.com> | 2003-10-02 05:04:38 +0000 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2003-10-02 05:04:38 +0000 |
commit | 3cfae2794784c1629dd98c0b600b2731e27a3f57 (patch) | |
tree | 5339db0e7df4b6f925788270d8c805111e729ae1 | |
parent | bfff951c310b0b1b4f2ddaf3d69549bd3bac2717 (diff) | |
download | fork-ledger-3cfae2794784c1629dd98c0b600b2731e27a3f57.tar.gz fork-ledger-3cfae2794784c1629dd98c0b600b2731e27a3f57.tar.bz2 fork-ledger-3cfae2794784c1629dd98c0b600b2731e27a3f57.zip |
*** empty log message ***
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | amount.cc | 55 | ||||
-rw-r--r-- | balance.cc | 92 | ||||
-rw-r--r-- | equity.cc | 16 | ||||
-rw-r--r-- | ledger.cc | 122 | ||||
-rw-r--r-- | ledger.h | 137 | ||||
-rw-r--r-- | ledger.texi | 218 | ||||
-rw-r--r-- | main.cc | 32 | ||||
-rw-r--r-- | parse.cc | 8 | ||||
-rw-r--r-- | register.cc | 3 | ||||
-rwxr-xr-x | report | 24 |
11 files changed, 421 insertions, 288 deletions
@@ -43,7 +43,7 @@ ledger.info: ledger.texi g++ $(CFLAGS) $(INCS) $(DFLAGS) -c -o $@ $< clean: - rm -f ledger *.o + rm -f ledger ledger.info *.o *~ .\#* rebuild: clean deps all @@ -1,20 +1,24 @@ -#include <sstream> +#include "ledger.h" +#include <sstream> #include <gmp.h> // GNU multi-precision library -#include "ledger.h" - namespace ledger { #define MAX_PRECISION 10 // must be 2 or higher ////////////////////////////////////////////////////////////////////// // -// 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. +// 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 a certain commodity, with +// an optional price per-unit for that commodity at the time the +// amount was stated. +// +// To create an amount, for example: +// +// amount * cost = create_amount("50.2 MSFT @ $100.50"); // class gmp_amount : public amount @@ -57,20 +61,21 @@ class gmp_amount : public amount } virtual void set_value(const amount * val); - virtual operator bool() const; + virtual bool is_zero() const; virtual void negate() { mpz_ui_sub(quantity, 0, quantity); } virtual void credit(const amount * other); - virtual void parse(const char * num); - virtual std::string as_str(bool full_prec) const; + virtual void parse(const std::string& num); + virtual const std::string as_str(bool full_prec) const; - friend amount * create_amount(const char * value, const amount * cost); + friend amount * create_amount(const std::string& value, + const amount * cost); }; -amount * create_amount(const char * value, const amount * cost) +amount * create_amount(const std::string& value, const amount * cost) { gmp_amount * a = new gmp_amount(); a->parse(value); @@ -277,7 +282,7 @@ void gmp_amount::set_value(const amount * val) mpz_clear(addend); } -gmp_amount::operator bool() const +bool gmp_amount::is_zero() const { mpz_t copy; mpz_init_set(copy, quantity); @@ -286,7 +291,7 @@ gmp_amount::operator bool() const round(copy, copy, quantity_comm->precision); bool zero = mpz_sgn(copy) == 0; mpz_clear(copy); - return ! zero; + return zero; } static std::string amount_to_str(const commodity * comm, const mpz_t val, @@ -416,7 +421,7 @@ static std::string amount_to_str(const commodity * comm, const mpz_t val, return s.str(); } -std::string gmp_amount::as_str(bool full_prec) const +const std::string gmp_amount::as_str(bool full_prec) const { std::ostringstream s; @@ -430,8 +435,11 @@ std::string gmp_amount::as_str(bool full_prec) const return s.str(); } -static void parse_number(mpz_t out, const char * num, commodity * comm) +static void parse_number(mpz_t out, const std::string& number, + commodity * comm) { + const char * num = number.c_str(); + if (char * p = std::strchr(num, '/')) { mpz_t numer; mpz_t val; @@ -559,7 +567,7 @@ static commodity * parse_amount(mpz_t out, const char * num, return comm; } -void gmp_amount::parse(const char * num) +void gmp_amount::parse(const std::string& number) { // Compile the regular expression used for parsing amounts static pcre * re = NULL; @@ -576,17 +584,20 @@ void gmp_amount::parse(const char * num) int ovector[60]; int matched; - matched = pcre_exec(re, NULL, num, std::strlen(num), 0, 0, ovector, 60); + matched = pcre_exec(re, NULL, number.c_str(), number.length(), + 0, 0, ovector, 60); if (matched > 0) { - quantity_comm = parse_amount(quantity, num, matched, ovector, 1); + quantity_comm = parse_amount(quantity, number.c_str(), matched, + ovector, 1); // If the following succeeded, then we have a price if (ovector[8 * 2] >= 0) { priced = true; - price_comm = parse_amount(price, num, matched, ovector, 9); + price_comm = parse_amount(price, number.c_str(), matched, + ovector, 9); } } else { - std::cerr << "Failed to parse amount: " << num << std::endl; + std::cerr << "Failed to parse amount: " << number << std::endl; } } @@ -16,51 +16,25 @@ static bool show_empty; static bool no_subtotals; static bool full_names; -static bool account_matches(const account * acct, - const std::list<mask>& regexps, - bool * true_match) -{ - bool match = false; - *true_match = false; - - if (show_children) { - for (const account * a = acct; a; a = a->parent) { - bool exclude = false; - if (matches(regexps, a->name, &exclude)) { - match = true; - *true_match = a == acct; - break; - } - if (exclude) - break; - } - } else { - match = matches(regexps, acct->as_str()); - if (match) - *true_match = matches(regexps, acct->name); - } - return match; -} - static void display_total(std::ostream& out, totals& balance, - account * acct, bool top_level, - const std::list<mask>& regexps) + account * acct, bool top_level) { bool displayed = false; - if (acct->checked == 1 && (show_empty || acct->balance)) { + if (acct->checked == 1 && + (show_empty || ! acct->balance.is_zero())) { displayed = true; - out << acct->balance; + acct->balance.print(out, 20); if (! no_subtotals && top_level) balance.credit(acct->balance); - if (acct->parent && ! no_subtotals && ! full_names) { + if (acct->parent && ! full_names && ! top_level) { for (const account * a = acct; a; a = a->parent) out << " "; out << acct->name << std::endl; } else { - out << " " << *acct << std::endl; + out << " " << acct->as_str() << std::endl; } } @@ -69,7 +43,7 @@ static void display_total(std::ostream& out, totals& balance, for (accounts_iterator i = acct->children.begin(); i != acct->children.end(); i++) - display_total(out, balance, (*i).second, ! displayed, regexps); + display_total(out, balance, (*i).second, ! displayed); } ////////////////////////////////////////////////////////////////////// @@ -77,7 +51,8 @@ static void display_total(std::ostream& out, totals& balance, // Balance reporting code // -void report_balances(int argc, char **argv, std::ostream& out) +void report_balances(int argc, char ** argv, regexps_t& regexps, + std::ostream& out) { show_children = false; show_empty = false; @@ -120,22 +95,33 @@ void report_balances(int argc, char **argv, std::ostream& out) 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 (regexps.empty()) { + if (! (show_children || ! acct->parent)) + acct->checked = 2; + else + acct->checked = 1; + } + else { + bool by_exclusion; + bool match = matches(regexps, acct->as_str(), + &by_exclusion); + if (! match) { + acct->checked = 2; + } + else if (by_exclusion) { + if (! (show_children || ! acct->parent)) + acct->checked = 2; + else + acct->checked = 1; + } + else { + acct->checked = 1; + } + } } - if (acct->checked == 2) - break; - else if (acct->checked == 3) - continue; - - acct->balance.credit((*x)->cost->street()); + if (acct->checked == 1) + acct->balance.credit((*x)->cost->street()); } } } @@ -148,13 +134,15 @@ void report_balances(int argc, char **argv, std::ostream& out) for (accounts_iterator i = main_ledger.accounts.begin(); i != main_ledger.accounts.end(); i++) - display_total(out, balance, (*i).second, true, regexps); + display_total(out, balance, (*i).second, true); // Print the total of all the balances shown - if (! no_subtotals && balance) - out << "--------------------" << std::endl - << balance << std::endl; + if (! no_subtotals && ! balance.is_zero()) { + out << "--------------------" << std::endl; + balance.print(out, 20); + out << std::endl; + } } } // namespace ledger @@ -2,10 +2,10 @@ namespace ledger { -static void equity_entry(std::ostream& out, account * acct, - const std::list<mask>& regexps) +static void equity_entry(account * acct, regexps_t& regexps, + std::ostream& out) { - if (acct->balance && + if (! acct->balance.is_zero() && (regexps.empty() || matches(regexps, acct->as_str()))) { entry opening; @@ -17,7 +17,8 @@ static void equity_entry(std::ostream& out, account * acct, for (totals::const_iterator i = acct->balance.amounts.begin(); i != acct->balance.amounts.end(); i++) { - if (! *((*i).second)) // skip if zero balance for the commodity + // Skip it, if there is a zero balance for the commodity + if ((*i).second->is_zero()) continue; xact = new transaction(); @@ -40,7 +41,7 @@ static void equity_entry(std::ostream& out, account * acct, for (accounts_iterator i = acct->children.begin(); i != acct->children.end(); i++) - equity_entry(out, (*i).second, regexps); + equity_entry((*i).second, regexps, out); } ////////////////////////////////////////////////////////////////////// @@ -50,7 +51,8 @@ static void equity_entry(std::ostream& out, account * acct, // balances. // -void equity_ledger(int argc, char **argv, std::ostream& out) +void equity_ledger(int argc, char ** argv, regexps_t& regexps, + std::ostream& out) { optind = 1; @@ -67,7 +69,7 @@ void equity_ledger(int argc, char **argv, std::ostream& out) for (accounts_iterator i = main_ledger.accounts.begin(); i != main_ledger.accounts.end(); i++) - equity_entry(out, (*i).second, regexps); + equity_entry((*i).second, regexps, out); } } // namespace ledger @@ -34,9 +34,8 @@ void entry::print(std::ostream& out, bool shortcut) const x != xacts.end(); x++) { #ifdef HUQUQULLAH - if ((*x)->acct->exempt_or_necessary && - (! shortcut || ! ledger::matches(main_ledger.huquq_categories, - (*x)->acct->as_str()))) + if ((*x)->exempt_or_necessary || + (! shortcut && (*x)->acct->exempt_or_necessary)) out << " !"; else #endif @@ -69,12 +68,12 @@ bool entry::validate(bool show_unaccounted) const if ((*x)->cost) balance.credit((*x)->cost->value()); - if (show_unaccounted && balance) { + if (show_unaccounted && ! balance.is_zero()) { std::cerr << "Unaccounted-for balances are:" << std::endl; balance.print(std::cerr, 20); std::cerr << std::endl << std::endl; } - return ! balance; // must balance to 0.0 + return balance.is_zero(); // must balance to 0.0 } bool entry::matches(const std::list<mask>& regexps) const @@ -117,33 +116,37 @@ void totals::credit(const totals& other) credit((*i).second); } -totals::operator bool() const +bool totals::is_zero() const { for (const_iterator i = amounts.begin(); i != amounts.end(); i++) - if (*((*i).second)) - return true; - return false; + if (! (*i).second->is_zero()) + return false; + return true; } void totals::print(std::ostream& out, int width) const { bool first = true; - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) - if (*((*i).second)) { - if (first) - first = false; - else - out << std::endl; - - out.width(width); - out << std::right << (*i).second->as_str(); - } + + for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { + if ((*i).second->is_zero()) + continue; + + if (first) + first = false; + else + out << std::endl; + + out.width(width); + out << std::right << (*i).second->as_str(); + } } // Print out the entire ledger that was read in, sorted by date. // This can be used to "wash" ugly ledger files. -void print_ledger(int argc, char *argv[], std::ostream& out) +void print_ledger(int argc, char ** argv, regexps_t& regexps, + std::ostream& out) { bool use_shortcuts = true; @@ -174,37 +177,40 @@ void print_ledger(int argc, char *argv[], std::ostream& out) (*i)->print(out, use_shortcuts); } -void record_regexp(char * pattern, std::list<mask>& regexps) +mask::mask(const std::string& pat) : exclude(false) { - bool exclude = false; - - char * pat = pattern; - if (*pat == '-') { + const char * p = pat.c_str(); + if (*p == '-') { exclude = true; - pat++; - while (std::isspace(*pat)) - pat++; + p++; + while (std::isspace(*p)) + p++; } - else if (*pat == '+') { - pat++; - while (std::isspace(*pat)) - pat++; + else if (*p == '+') { + p++; + while (std::isspace(*p)) + p++; } + pattern = p; const char *error; int erroffset; - pcre * re = pcre_compile(pat, PCRE_CASELESS, &error, &erroffset, NULL); - if (! re) + regexp = pcre_compile(pattern.c_str(), PCRE_CASELESS, + &error, &erroffset, NULL); + if (! regexp) std::cerr << "Warning: Failed to compile regexp: " << pattern << std::endl; - else - regexps.push_back(mask(exclude, re)); } -void read_regexps(const char * path, std::list<mask>& regexps) +void record_regexp(const std::string& pattern, regexps_t& regexps) +{ + regexps.push_back(mask(pattern)); +} + +void read_regexps(const std::string& path, regexps_t& regexps) { - if (access(path, R_OK) != -1) { - std::ifstream file(path); + if (access(path.c_str(), R_OK) != -1) { + std::ifstream file(path.c_str()); while (! file.eof()) { char buf[80]; @@ -215,14 +221,20 @@ void read_regexps(const char * path, std::list<mask>& regexps) } } -bool matches(const std::list<mask>& regexps, const std::string& str, - bool * exclude) +bool matches(const regexps_t& regexps, const std::string& str, + bool * by_exclusion) { + assert(! regexps.empty()); + // If the first pattern is an exclude, we assume all patterns match - // if they don't match the exclude. If the first pattern is an - // include, then only accounts matching the include will match. + // if they don't match the exclude -- and by_exclusion will be set + // to true to reflect this "by default" behavior. But if the first + // pattern is an include, only accounts matching the include will + // match, and these are a positive match. bool match = (*regexps.begin()).exclude; + if (match && by_exclusion) + *by_exclusion = true; for (std::list<mask>::const_iterator r = regexps.begin(); r != regexps.end(); @@ -230,8 +242,8 @@ bool matches(const std::list<mask>& regexps, const std::string& str, int ovec[3]; if (pcre_exec((*r).regexp, NULL, str.c_str(), str.length(), 0, 0, ovec, 3) >= 0) { - if (exclude) - *exclude = (*r).exclude; + if (by_exclusion) + *by_exclusion = (*r).exclude; match = ! (*r).exclude; } } @@ -261,12 +273,12 @@ state::~state() #endif // DO_CLEANUP -void state::record_price(const char * setting) +void state::record_price(const std::string& setting) { char buf[128]; - std::strcpy(buf, setting); + std::strcpy(buf, setting.c_str()); - assert(std::strlen(setting) < 128); + assert(setting.length() < 128); char * c = buf; char * p = std::strchr(buf, '='); @@ -278,14 +290,14 @@ void state::record_price(const char * setting) } } -account * state::find_account(const char * name, bool create) +account * state::find_account(const std::string& name, bool create) { accounts_iterator i = accounts_cache.find(name); if (i != accounts_cache.end()) return (*i).second; - char * buf = new char[std::strlen(name) + 1]; - std::strcpy(buf, name); + char * buf = new char[name.length() + 1]; + std::strcpy(buf, name.c_str()); account * current = NULL; for (char * tok = std::strtok(buf, ":"); @@ -294,8 +306,10 @@ account * state::find_account(const char * name, bool create) if (! current) { accounts_iterator i = accounts.find(tok); if (i == accounts.end()) { - if (! create) + if (! create) { + delete[] buf; return NULL; + } current = new account(tok); accounts.insert(accounts_entry(tok, current)); } else { @@ -304,8 +318,10 @@ account * state::find_account(const char * name, bool create) } else { accounts_iterator i = current->children.find(tok); if (i == current->children.end()) { - if (! create) + if (! create) { + delete[] buf; return NULL; + } current = new account(tok, current); current->parent->children.insert(accounts_entry(tok, current)); } else { @@ -1,5 +1,5 @@ #ifndef _LEDGER_H -#define _LEDGER_H "$Revision: 1.16 $" +#define _LEDGER_H "$Revision: 1.17 $" ////////////////////////////////////////////////////////////////////// // @@ -26,70 +26,6 @@ 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 * (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 * (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 * 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; @@ -127,9 +63,9 @@ class amount virtual bool has_price() const = 0; virtual void set_value(const amount * pr) = 0; - // Test if non-zero + // Test if the quantity is zero - virtual operator bool() const = 0; + virtual bool is_zero() const = 0; // Assignment @@ -138,27 +74,28 @@ class amount // String conversion routines - virtual void parse(const char * num) = 0; - virtual std::string as_str(bool full_prec = false) const = 0; + virtual void parse(const std::string& num) = 0; + virtual const std::string as_str(bool full_prec = false) const = 0; }; -extern amount * create_amount(const char * value, +extern amount * create_amount(const std::string& value, const amount * cost = NULL); struct mask { - bool exclude; - pcre * regexp; + bool exclude; + std::string pattern; + pcre * regexp; - mask(bool exc, pcre * re) : exclude(exc), regexp(re) {} + mask(const std::string& pattern); }; -extern std::list<mask> regexps; +typedef std::list<mask> regexps_t; -extern void record_regexp(char * pattern, std::list<mask>& regexps); -extern void read_regexps(const char * path, std::list<mask>& regexps); -extern bool matches(const std::list<mask>& regexps, - const std::string& str, bool * exclude = NULL); +void record_regexp(const std::string& pattern, regexps_t& regexps); +void read_regexps(const std::string& path, regexps_t& regexps); +bool matches(const regexps_t& regexps, const std::string& str, + bool * by_exclusion = NULL); struct account; @@ -187,6 +124,7 @@ struct transaction #endif }; + struct entry { std::time_t date; @@ -249,7 +187,7 @@ struct totals } void credit(const totals& other); - operator bool() const; + bool is_zero() const; void print(std::ostream& out, int width) const; @@ -259,13 +197,6 @@ struct totals } }; -template<class Traits> -std::basic_ostream<char, Traits> & -operator<<(std::basic_ostream<char, Traits>& out, const totals& t) { - t.print(out, 20); - return out; -} - typedef std::map<const std::string, account *> accounts_t; typedef accounts_t::iterator accounts_iterator; @@ -276,10 +207,14 @@ struct account account * parent; std::string name; +#ifdef READ_GNUCASH commodity * comm; // default commodity for this account - totals balance; +#endif + totals balance; // optional, parse-time computed balance - int checked; + mutable std::string full_name; + + int checked; // 'balance' uses this for speed's sake #ifdef HUQUQULLAH bool exempt_or_necessary; #endif @@ -301,25 +236,21 @@ struct account const std::string as_str() const { if (! parent) return name; - else - return parent->as_str() + ":" + name; + else if (full_name.empty()) + full_name = parent->as_str() + ":" + name; + + return full_name; } }; -template<class Traits> -std::basic_ostream<char, Traits> & -operator<<(std::basic_ostream<char, Traits>& out, const account& a) { - return (out << a.as_str()); -} - struct state { - commodities_t commodities; - accounts_t accounts; - accounts_t accounts_cache; // maps full names to accounts - entries_t entries; - totals prices; + commodities_t commodities; + accounts_t accounts; + accounts_t accounts_cache; // maps full names to accounts + entries_t entries; + totals prices; #ifdef HUQUQULLAH bool compute_huquq; @@ -336,8 +267,8 @@ struct state ~state(); #endif - void record_price(const char * setting); - account * find_account(const char * name, bool create = true); + void record_price(const std::string& setting); + account * find_account(const std::string& name, bool create = true); }; extern state main_ledger; diff --git a/ledger.texi b/ledger.texi index 722c63d1..aba24264 100644 --- a/ledger.texi +++ b/ledger.texi @@ -1,5 +1,5 @@ \input texinfo @c -*-texinfo-*- -@comment $Id: ledger.texi,v 1.7 2003/10/02 00:07:14 johnw Exp $ +@comment $Id: ledger.texi,v 1.8 2003/10/02 05:04:38 johnw Exp $ @comment %**start of header @setfilename ledger.info @@ -24,17 +24,24 @@ @contents @ifnottex -@node Top +@node Top, Introduction, (dir), (dir) @top Ledger Accouting Tool @c @insertcopying @end ifnottex +@menu +* Introduction:: +* Keeping a ledger:: +* Computing Huqúqu'lláh:: +@end menu + +@node Introduction, Keeping a ledger, Top, Top @chapter Introduction @code{ledger} is an accouting tool that has the chutzpah to exist. It provides not one bell or whistle for the money, and returns the user -back to the days before user interfaces were even a twinkle on their +to the days before user interfaces were even a twinkle in their father's CRT. What it does do is provide a double-entry accouting ledger with all of @@ -138,6 +145,7 @@ Your usage of @code{ledger} will have two parts: Keeping the ledger, and using the @code{ledger} tool to provide you with information summaries derived from your ledger's entries. +@node Keeping a ledger, Computing Huqúqu'lláh, Introduction, Top @chapter Keeping a ledger The most important part of accounting is keeping a good ledger. If @@ -192,7 +200,79 @@ the balanced amount, if it is the same as the first line: For this entry, @code{ledger} will figure out that $-23.00 must come from ``Assets:Checking'' in order to balance the entry. -@section Commodities and currencies +@menu +* Credits and Debits:: +* Commodities and Currencies:: +* Accounts and Inventories:: +* Understanding Equity:: +@end menu + +@node Credits and Debits, Commodities and Currencies, Keeping a ledger, Keeping a ledger +@section Credits and Debits + +Credit and debit are simple enough terms in themselves, but the usages +of the modern world have made them very hard to puzzle out. + +Basically, a credit means you add something to an account, and a debit +means you take away. A debit card is correctly name: From your point +of view, it debits your checking account every time you use it. + +The credit card is strangely named, because you have to look at it +from the merchant's point of view: Every time you use it, it credit's +@emph{his} account right away. This was a giant leap from the days of +cash and checks, when the only other way to supply immediate credit +was by a wire transfer. But a credit card does not credit you +anything at all. In fact, from your point of view, it should be +called a liability card, since it increases your liability to the +issuing bank every time you use it. + +In @code{ledger}, credits and debits are given as they are, which +means that sometimes you will see a minus sign where you don't expect +one. For example, when you get paid, in order to credit your bank +account, you need to debit an income account: + +@example +9/29 My Employer + Assets:Checking $500.00 + Income:Salary $-500.00 +@end example + +But wait, you say, why is the Income a negative figure? And when you +look at the balance totals for your ledger, you will certainly be +surprised to see Expenses as a positive figure, and Income as a +negative figure. Isn't that the opposite of how it should look? + +It may take getting used to, but to properly use a general ledger you +will need to think in terms of correct debits and credits. Rather +than @code{ledger} ``fixing'' the minus signs, let's understand why +they are there. + +When you earn money, the money has to come from somewhere. Let's call +that somewhere ``society''. In order for society to give you an +income, you must take money away from society (debit) in order to put +it into your bank (credit). When you then spend that money, it leaves +your bank account (debit) and goes back to society (credit). This is +why Income will appear negative---it reflects the money you have drawn +from society---and why Expenses will be positive---it is the amount +you've given back. These credits and debits will always cancel each +other out in the end, because you don't have the ability to create new +money: It must always come from somewhere, and in the end must always +leave. This is the beginning of economy, after which the explanation +gets terribly difficult. + +Based on that explanation, here's another way to look at your balance +report: Every negative figure means that that account or person or +place has less money now than when you started your ledger; and every +positive figure means that that account or person or place has more +money now that when you started your ledger. Make sense? + +Alos, credit cards will have a negative value, because you are +spending @emph{from} them (debit) in order pay someone else (credit). +They are called credit cards because you are able to instantly credit +that other person, by simply waving a card. + +@node Commodities and Currencies, Accounts and Inventories, Credits and Debits, Keeping a ledger +@section Commodities and Currencies @code{ledger} makes no assumptions about the commodities you use; it only requires that you specify a commodity. The commodity may be any @@ -292,6 +372,7 @@ Euro=DM 0.75 This is a roundabout way of reporting AAPL shares in their Deutsch Mark equivalent. +@node Accounts and Inventories, Understanding Equity, Commodities and Currencies, Keeping a ledger @section Accounts and Inventories Since @code{ledger}'s accounts and commodity system is so flexible, @@ -329,8 +410,120 @@ would look like: Now you've turned 2 steaks into 15 gold, courtesy of your customer, Sturm Brightblade. -@chapter Using @code{ledger} +@node Understanding Equity, , Accounts and Inventories, Keeping a ledger +@section Understanding Equity + +The most confusing entry in any ledger will be your equity +account---because starting balances can't come out of nowhere. + +When you first start your ledger, you will likely already have money +in some of your accounts. Let's say there's $100 in your checking +account; then add an entry to your ledger to reflect this amount. +Where will money come from? The answer: your equity. + +@example +10/2 Opening Balance + Assets:Checking $100.00 + Equity:Opening Balances $-100.00 +@end example + +But what is equity? You may have heard of equity when people talked +about house mortgages, as ``the part of the house that you own''. +Basically, equity is like the value of something. If you own a car +worth $5000, then you have $5000 in equity in that car. In order to +turn that car (a commodity) into a cash flow, or a credit to your bank +account, you will have to debit the equity by selling it. + +When you start a ledger, you are probably already worth something. +Your net worth is your current equity. By transferring the money in +the ledger from your equity to your bank accounts, you are crediting +the ledger account based on your prior equity value. That is why, +when you look at the balance report, you will see a large negative +number for Equity that never changes: Because that is what you were +worth (what you debited from yourself in order to start the ledger) +before the money started moving around. If the total positive value +of your assets is greater than the absolute value of your starting +equity, it means you are making money. + +Clear as mud? Keep thinking about it. Until you figure it out, put +``-- -Equity'' at the end of your balance command, to remove the +confusing figure from the totals. + +@chapter Using the @code{ledger} Tool + +Now that you have an orderly and well-organized general ledger, it's +time to start generating some orderly and well-organized reports. +This is where the @code{ledger} tool comes in. With it, you can +balance your checkbook, see where your money is going, tell whether +you've made a profit this year, and even compute the present day value +of your retirement accounts. And all with the simplest of interfaces: +the command-line. + +The most often used command will be the @code{balance} command: + +@example +/home/johnw $ export LEDGER=/home/johnw/doc/finance/ledger.dat +/home/johnw $ ledger balance +@end example + +Here I've set my @code{LEDGER} environment variable to point to where +my ledger file is hiding. Thereafter, I needn't specify it again. + +The balance command prints out the summarized balances of all my +top-level accounts, excluding sub-accounts. In order to see the +balances for a specific account, just specify a regular expression +after the balance command: + +@example +/home/johnw $ ledger balance expenses:food +@end example + +This will show all the money that's been spent on food, since the +beginning of the ledger. For food spending just this month +(September), use: + +@example +/home/johnw $ ledger -d sep balance expenses:food +@end example + +Or maybe I want to see all of my assets, in which case the -s (show +sub-accounts) option comes in handy: + +@example +/home/johnw $ ledger balance -s +@end example + +To exclude a particular account, use a regular expression with a +leading minus sign. The following will show all expenses, but without +food spending: + +@example +/home/johnw $ ledger balance expenses -food +@end example + +If you want to show all accounts but for one account, remember to use +@samp{--} to separate the exclusion pattern from the options list: + +@example +/home/johnw $ ledger balance -- -equity +@end example + +@chapter Using GnuCash to Keep Your Ledger + +The @code{ledger} tool is fast and simple, but it gives you no special +method of actually editing the ledger. It assumes you know how to use +a text editor, and like doing so. Perhaps an Emacs mode will appear +someday soon to make editing @code{ledger}'s data files much easier. + +Until then, you are free to use GnuCash to maintain your ledger, and +the @code{ledger} program for querying and reporting on the contents +of that ledger. It takes a little longer to parse the XML data format +that GnuCash uses, but the end result is identical. + +Then again, why would anyone use a Gnome-centric, 35 megabyte behemoth +to edit their data, and a 65 kilobyte executable to query it@dots{} +@node Computing Huqúqu'lláh, , Keeping a ledger, Top @chapter Computing Huqúqu'lláh As a Bahá'í, I need to compute Huqúqu'lláh on some of my assets. The @@ -374,20 +567,7 @@ That's it. To see how much Huqúq is currently owed based on your ledger data, type: @example -/home/johnw $ ledger -f ledger.dat balance huquq +/home/johnw $ ledger -f ledger.dat balance ^huquq @end example -Not sure if you should pay yet? Go to your newspaper, or look on the -Web, and find the current price of gold per ounce. Then pass this -figure to the @samp{-G} option. If it were $357.10, you would use: - -@example -/home/johnw $ ledger -f ledger.dat balance -G 357.10 huquq -@end example - -Now your balance report will be given in mi@underline{th}qáls of gold, -not dollars. If the balance on your Huqúqu'lláh account is more than --19 mi@underline{th}qáls (remember, it is a liability account, so -amounts are negative), then get out your checkbook. - @bye @@ -8,10 +8,14 @@ namespace ledger { extern bool parse_gnucash(std::istream& in, bool compute_balances); #endif - extern void report_balances(int argc, char **argv, std::ostream& out); - extern void print_register(int argc, char **argv, std::ostream& out); - extern void print_ledger(int argc, char *argv[], std::ostream& out); - extern void equity_ledger(int argc, char **argv, std::ostream& out); + extern void report_balances(int argc, char ** argv, regexps_t& regexps, + std::ostream& out); + extern void print_register(int argc, char ** argv, regexps_t& regexps, + std::ostream& out); + extern void print_ledger(int argc, char ** argv, regexps_t& regexps, + std::ostream& out); + extern void equity_ledger(int argc, char ** argv, regexps_t& regexps, + std::ostream& out); bool show_cleared; bool get_quotes; @@ -75,7 +79,7 @@ static const char *formats[] = { NULL }; -static bool parse_date(const char * date_str, std::time_t * result) +static bool parse_date(const std::string& date_str, std::time_t * result) { struct std::tm when; @@ -84,7 +88,7 @@ static bool parse_date(const char * date_str, std::time_t * result) for (const char ** f = formats; *f; f++) { memset(&when, INT_MAX, sizeof(struct std::tm)); - if (strptime(date_str, *f, &when)) { + if (strptime(date_str.c_str(), *f, &when)) { when.tm_hour = 0; when.tm_min = 0; when.tm_sec = 0; @@ -113,12 +117,12 @@ static bool parse_date(const char * date_str, std::time_t * result) // Command-line parser and top-level logic. // -int main(int argc, char *argv[]) +int main(int argc, char * argv[]) { - // Parse the command-line options - std::istream * file = NULL; + regexps_t regexps; + #ifdef HUQUQULLAH bool compute_huquq = true; #endif @@ -126,6 +130,8 @@ int main(int argc, char *argv[]) have_ending = false; show_cleared = false; + // Parse the command-line options + int c; while (-1 != (c = getopt(argc, argv, "+b:e:d:cChHwf:i:p:Pv"))) { switch (char(c)) { @@ -328,13 +334,13 @@ int main(int argc, char *argv[]) // Process the command if (command == "balance") - report_balances(argc - optind, &argv[optind], std::cout); + report_balances(argc - optind, &argv[optind], regexps, std::cout); else if (command == "register") - print_register(argc - optind, &argv[optind], std::cout); + print_register(argc - optind, &argv[optind], regexps, std::cout); else if (command == "print") - print_ledger(argc - optind, &argv[optind], std::cout); + print_ledger(argc - optind, &argv[optind], regexps, std::cout); else if (command == "equity") - equity_ledger(argc - optind, &argv[optind], std::cout); + equity_ledger(argc - optind, &argv[optind], regexps, std::cout); } // main.cc ends here. @@ -114,7 +114,9 @@ static void finalize_entry(entry * curr, bool compute_balances) } #ifdef HUQUQULLAH - if (! main_ledger.compute_huquq || ! (*x)->exempt_or_necessary) + if (! main_ledger.compute_huquq || + ! ((*x)->exempt_or_necessary || + (*x)->acct->exempt_or_necessary)) continue; // Reflect 19% of the exempt or necessary transaction in the @@ -307,10 +309,6 @@ bool parse_ledger(std::istream& in, bool compute_balances) #endif xact->acct = main_ledger.find_account(p); -#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); diff --git a/register.cc b/register.cc index e0128d10..bc7f6b06 100644 --- a/register.cc +++ b/register.cc @@ -28,7 +28,8 @@ static std::string truncated(const std::string& str, int width) // Register printing code // -void print_register(int argc, char **argv, std::ostream& out) +void print_register(int argc, char ** argv, regexps_t& regexps, + std::ostream& out) { optind = 1; @@ -1,20 +1,20 @@ #!/bin/bash -binary=ledger -LEDGER=$HOME/doc/finance/ledger.dat - -line="$binary -f $ledger" - command=$1 shift case "$command" in - balance) $line "$@" balance -s -- -Equity -Income -Expenses -Retirement ;; - worth) $line "$@" balance assets liabilities ;; - profit) $line "$@" balance income expense ;; - spending) $line "$@" balance -F food movies gas tips \ + balance) ledger "$@" balance -- -Equity -Income -Expenses -Retirement ;; + worth) ledger "$@" balance assets liabilities ;; + profit) ledger "$@" balance income expense ;; + spending) ledger "$@" balance -F food movies gas tips \ health supplies -insurance -vacation ;; - huquq) $line "$@" balance ^huquq ;; - gold) $line "$@" balance -G $1 ^huquq ;; - equity) $line "$@" equity -- -^Income -^Expenses -^Equity ;; + monthly_spending) + for i in jan feb mar apr may jun jul aug sep oct nov dec + do + echo $i: + $0 spending -d $i + done ;; + huquq) ledger "$@" balance -n ^huquq ;; + equity) ledger "$@" equity -- -^Income -^Expenses -^Equity ;; esac |