diff options
author | John Wiegley <johnw@newartisans.com> | 2004-08-15 17:08:56 -0400 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2004-08-15 17:08:56 -0400 |
commit | 7189b181ef3e0da74d992da87771fa26399391bc (patch) | |
tree | 3c6b677a54161fb63cb169c93d9eade0b316b4da | |
parent | 78c44b5e795a98baacfbc56943b784370956cbca (diff) | |
download | fork-ledger-7189b181ef3e0da74d992da87771fa26399391bc.tar.gz fork-ledger-7189b181ef3e0da74d992da87771fa26399391bc.tar.bz2 fork-ledger-7189b181ef3e0da74d992da87771fa26399391bc.zip |
further restructuring
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | amount.cc | 23 | ||||
-rw-r--r-- | amount.h | 14 | ||||
-rw-r--r-- | config.cc | 348 | ||||
-rw-r--r-- | config.h | 54 | ||||
-rw-r--r-- | debug.cc | 18 | ||||
-rw-r--r-- | ledger.cc | 10 | ||||
-rw-r--r-- | ledger.h | 4 | ||||
-rw-r--r-- | main.cc | 653 | ||||
-rw-r--r-- | quotes.cc | 55 | ||||
-rw-r--r-- | quotes.h | 29 | ||||
-rwxr-xr-x | scripts/confirm.py | 2 | ||||
-rw-r--r-- | textual.cc | 13 | ||||
-rw-r--r-- | walk.cc | 4 | ||||
-rw-r--r-- | walk.h | 17 |
15 files changed, 700 insertions, 546 deletions
@@ -3,12 +3,14 @@ CODE = account.cc \ autoxact.cc \ balance.cc \ binary.cc \ + config.cc \ datetime.cc \ debug.cc \ error.cc \ format.cc \ ledger.cc \ option.cc \ + quotes.cc \ textual.cc \ valexpr.cc \ walk.cc @@ -776,27 +776,24 @@ void amount_t::read_quantity(std::istream& in) } } -void (*commodity_t::updater)(commodity_t * commodity, - const std::time_t date, - const amount_t& price, - const std::time_t moment) = NULL; +commodity_t::updater_t * commodity_t::updater = NULL; +commodities_map commodity_t::commodities; +commodity_t * commodity_t::null_commodity = + commodity_t::find_commodity("", true); -commodities_map commodity_t::commodities; -commodity_t * commodity_t::null_commodity = - commodity_t::find_commodity("", true); - -struct cleanup_commodities +static struct cleanup_commodities { ~cleanup_commodities() { + if (commodity_t::updater) + delete commodity_t::updater; + for (commodities_map::iterator i = commodity_t::commodities.begin(); i != commodity_t::commodities.end(); i++) delete (*i).second; } -}; - -static cleanup_commodities cleanup; +} _cleanup; commodity_t * commodity_t::find_commodity(const std::string& symbol, bool auto_create) @@ -820,7 +817,7 @@ amount_t commodity_t::value(const std::time_t moment) amount_t price; if (updater) - updater(this, age, price, moment); + (*updater)(this, age, price, moment); for (history_map::reverse_iterator i = history.rbegin(); i != history.rend(); @@ -188,6 +188,15 @@ typedef std::pair<const std::string, commodity_t *> commodities_pair; class commodity_t { public: + class updater_t { + public: + virtual ~updater_t() {} + virtual void operator()(commodity_t * commodity, + const std::time_t date, + const amount_t& price, + const std::time_t moment) = 0; + }; + typedef unsigned short ident_t; std::string symbol; @@ -202,10 +211,7 @@ class commodity_t // If set, this global function pointer is called to determine // whether prices have been updated in the meanwhile. - static void (*updater)(commodity_t * commodity, - const std::time_t date, - const amount_t& price, - const std::time_t moment); + static updater_t * updater; // This map remembers all commodities that have been // defined thus far. diff --git a/config.cc b/config.cc new file mode 100644 index 00000000..d96ce291 --- /dev/null +++ b/config.cc @@ -0,0 +1,348 @@ +#include "config.h" +#include "option.h" + +namespace ledger { + +std::auto_ptr<config_t> config(new config_t); + +const std::string bal_fmt = "%20T %2_%-n\n"; +const std::string reg_fmt + = "%D %-.20P %-.22N %12.66t %12.80T\n\ +%/ %-.22N %12.66t %12.80T\n"; +const std::string plot_value_fmt = "%D %t\n"; +const std::string plot_total_fmt = "%D %T\n"; +const std::string print_fmt + = "\n%D %X%C%P\n %-34N %12o\n%/ %-34N %12o\n"; +const std::string equity_fmt + = "\n%D %X%C%P\n%/ %-34N %12t\n"; + +config_t::config_t() +{ + if (const char * p = std::getenv("HOME")) + init_file = cache_file = price_db = p; + + init_file += "/.ledgerrc"; + cache_file += "/.ledger"; + price_db += "/.pricedb"; + + value_expr = "a"; + total_expr = "T"; + pricing_leeway = 24 * 3600; + show_subtotals = true; + show_expanded = false; + show_related = false; + show_inverted = false; + show_empty = false; + days_of_the_week = false; + show_revalued = false; + show_revalued_only = false; + download_quotes = false; +} + +static void show_version(std::ostream& out) +{ + out + << "Ledger " << ledger::version << ", the command-line accounting tool\n\n" + << "Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.\n\n" + << "This program is made available under the terms of the BSD Public\n" + << "License. See the LICENSE file included with the distribution for\n" + << "details and disclaimer.\n"; +} + +void option_help(std::ostream& out) +{ + out + << "usage: ledger [options] COMMAND [options] [REGEXPS]\n\n\ +Basic options:\n\ + -h, --help display this help text\n\ + -v, --version display version information\n\ + -i, --init FILE initialize ledger by loading FILE\n\ + -f, --file FILE specify pathname of ledger data file\n\ + -o, --output FILE write all output to FILE\n\ + -p, --set-price CONV specifies commodity conversion: COMM=AMOUNT\n\n\ +Report filtering:\n\ + -b, --begin-date DATE specify a beginning date\n\ + -e, --end-date DATE specify an ending date\n\ + -c, --current do not show future entries (same as -e TODAY)\n\ + -C, --cleared show only cleared transactions and balances\n\ + -U, --uncleared show only uncleared transactions and balances\n\ + -R, --real do not consider virtual transactions: real only\n\n\ +Output customization:\n\ + -F, --format STR \n\ + -y, --date-format STR \n\ + -E, --empty balance: also show accounts that total to zero\n\ + -n, --collapse balance: no parent account totals; register: collapse\n\ + -s, --show-all balance: show sub-accounts; register: show subtotals\n\ + -S, --sort EXPR sort report according to value EXPR\n\ + -r, --related \n\ + -z, --interval EXPR \n\ + -w, --dow print register using day of week sub-totals\n\ + -W, --weekly \" \" weekly sub-totals\n\ + -M, --monthly \" \" monthly sub-totals\n\ + -Y, --yearly \" \" yearly sub-totals\n\ + -l, --limit EXPR don't calculate entries for which EXPR yields 0\n\ + -d, --display EXPR don't print entries for which EXPR yields 0\n\ + -t, --value EXPR \n\ + -T, --total EXPR \n\ + -j, --value-data \n\ + -J, --total-data \n\n\ +Commodity reporting:\n\ + -P, --price-db FILE sets the price database\n\ + -L, --price-exp MINS with -Q, fetch quotes only if data is older than MINS\n\ + -Q, --download download price information from the Internet\n\ + (works by running \"getquote SYMBOL\")\n\ + -O, --quantity \n\ + -B, --basis report cost basis of commodities\n\ + -V, --market report the market value of commodities\n\ + -G, --gain \n\ + -A, --average \n\ + -D, --deviation \n\ + -X, --trend \n\ + -Z, --weighted-trend \n\n\ +Commands:\n\ + balance show balance totals\n\ + register display a register for ACCOUNT\n\ + print print all ledger entries\n\ + entry output a newly formed entry, based on arguments\n\ + equity output equity entries for specified accounts\n"; +} + +////////////////////////////////////////////////////////////////////// +// +// Basic options + +DEF_OPT_HANDLERS(); + +OPT_BEGIN(help, "h", false) { + option_help(std::cout); + std::exit(0); +} OPT_END(help); + +OPT_BEGIN(version, "v", false) { + show_version(std::cout); + std::exit(0); +} OPT_END(version); + +OPT_BEGIN(init, "i:", true) { + config->init_file = optarg; +} OPT_END(init); + +OPT_BEGIN(file, "f:", true) { + char * buf = new char[std::strlen(optarg) + 1]; + std::strcpy(buf, optarg); + for (char * p = std::strtok(buf, ":"); + p; + p = std::strtok(NULL, ":")) + config->files.push_back(p); + delete[] buf; +} OPT_END(file); + +OPT_BEGIN(cache, ":", false) { + config->cache_file = optarg; +} OPT_END(cache); + +OPT_BEGIN(output, "o:", false) { + if (std::string(optarg) != "-") + config->output_file = optarg; +} OPT_END(output); + +OPT_BEGIN(set_price, "p:", true) { + if (std::strchr(optarg, '=')) + config->price_settings.push_back(optarg); + else + std::cerr << "Error: Invalid price setting: " << optarg << std::endl; +} OPT_END(set_price); + +////////////////////////////////////////////////////////////////////// +// +// Report filtering + +OPT_BEGIN(begin_date, "b:", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "(d>=["; + config->predicate += optarg; + config->predicate += "])"; +} OPT_END(begin_date); + +OPT_BEGIN(end_date, "e:", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "(d<["; + config->predicate += optarg; + config->predicate += "])"; +} OPT_END(end_date); + +OPT_BEGIN(current, "c", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "(d<t)"; +} OPT_END(current); + +OPT_BEGIN(cleared, "C", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "X"; +} OPT_END(cleared); + +OPT_BEGIN(uncleared, "U", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "!X"; +} OPT_END(uncleared); + +OPT_BEGIN(real, "R", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "R"; +} OPT_END(real); + +////////////////////////////////////////////////////////////////////// +// +// Output customization + +OPT_BEGIN(format, "F:", false) { + config->format_string = optarg; +} OPT_END(format); + +OPT_BEGIN(date_format, "y:", false) { + config->date_format = optarg; +} OPT_END(date_format); + +OPT_BEGIN(empty, "E", false) { + config->show_empty = true; +} OPT_END(empty); + +OPT_BEGIN(collapse, "n", false) { + config->show_subtotals = false; +} OPT_END(collapse); + +OPT_BEGIN(show_all, "s", false) { + config->show_expanded = true; +} OPT_END(show_all); + +OPT_BEGIN(sort, "S:", false) { + config->sort_string = optarg; +} OPT_END(sort); + +OPT_BEGIN(related, "r", false) { + config->show_related = true; +} OPT_END(related); + +OPT_BEGIN(interval, "z:", false) { + config->interval_text = optarg; +} OPT_END(interval); + +OPT_BEGIN(weekly, "W", false) { + config->interval_text = "weekly"; +} OPT_END(weekly); + +OPT_BEGIN(dow, "w", false) { + config->days_of_the_week = true; +} OPT_END(dow); + +OPT_BEGIN(monthly, "M", false) { + config->interval_text = "monthly"; +} OPT_END(monthly); + +OPT_BEGIN(yearly, "Y", false) { + config->interval_text = "yearly"; +} OPT_END(yearly); + +OPT_BEGIN(limit, "l:", false) { + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += "("; + config->predicate += optarg; + config->predicate += ")"; +} OPT_END(limit); + +OPT_BEGIN(display, "d:", false) { + if (! config->display_predicate.empty()) + config->display_predicate += "&"; + config->display_predicate += "("; + config->display_predicate += optarg; + config->display_predicate += ")"; +} OPT_END(display); + +OPT_BEGIN(value, "t:", false) { + config->value_expr = optarg; +} OPT_END(value); + +OPT_BEGIN(total, "T:", false) { + config->total_expr = optarg; +} OPT_END(total); + +OPT_BEGIN(value_data, "j", false) { + config->value_expr = "S" + config->value_expr; + config->format_string = plot_value_fmt; +} OPT_END(value_data); + +OPT_BEGIN(total_data, "J", false) { + config->total_expr = "S" + config->total_expr; + config->format_string = plot_total_fmt; +} OPT_END(total_data); + +////////////////////////////////////////////////////////////////////// +// +// Commodity reporting + +OPT_BEGIN(price_db, "P:", false) { + config->price_db = optarg; +} OPT_END(price_db); + +OPT_BEGIN(price_exp, "L:", false) { + config->pricing_leeway = std::atol(optarg) * 60; +} OPT_END(price_exp); + +OPT_BEGIN(download, "Q", false) { + config->download_quotes = true; +} OPT_END(download); + +OPT_BEGIN(quantity, "O", false) { + config->value_expr = "a"; + config->total_expr = "T"; +} OPT_END(quantity); + +OPT_BEGIN(basis, "B", false) { + config->value_expr = "c"; + config->total_expr = "C"; +} OPT_END(basis); + +OPT_BEGIN(market, "V", false) { + config->show_revalued = true; + + config->value_expr = "v"; + config->total_expr = "V"; +} OPT_END(market); + +OPT_BEGIN(gain, "G", false) { + config->show_revalued = + config->show_revalued_only = true; + + config->value_expr = "a"; + config->total_expr = "G"; +} OPT_END(gain); + +OPT_BEGIN(average, "A", false) { + config->value_expr = "a"; + config->total_expr = "MT"; +} OPT_END(average); + +OPT_BEGIN(deviation, "D", false) { + config->value_expr = "a"; + config->total_expr = "DMT"; +} OPT_END(deviation); + +OPT_BEGIN(trend, "X", false) { + config->value_expr = "a"; + config->total_expr = "MDMT"; +} OPT_END(trend); + +OPT_BEGIN(weighted_trend, "Z", false) { + config->value_expr = "a"; + config->total_expr + = "MD(MT/(1+(((t-d)/(30*86400))<0?0:((t-d)/(30*86400)))))"; +} OPT_END(weighted_trend); + +} // namespace ledger diff --git a/config.h b/config.h new file mode 100644 index 00000000..c026bdec --- /dev/null +++ b/config.h @@ -0,0 +1,54 @@ +#ifndef _AUTOXACT_H +#define _AUTOXACT_H + +#include "ledger.h" + +#include <iostream> +#include <memory> + +namespace ledger { + +extern const std::string bal_fmt; +extern const std::string reg_fmt; +extern const std::string plot_value_fmt; +extern const std::string plot_total_fmt; +extern const std::string print_fmt; +extern const std::string equity_fmt; + +struct config_t +{ + strings_list files; + strings_list price_settings; + std::string init_file; + std::string cache_file; + std::string price_db; + std::string output_file; + std::string predicate; + std::string display_predicate; + std::string interval_text; + std::string format_string; + std::string date_format; + std::string sort_string; + std::string value_expr; + std::string total_expr; + unsigned long pricing_leeway; + bool show_subtotals; + bool show_expanded; + bool show_related; + bool show_inverted; + bool show_empty; + bool days_of_the_week; + bool show_revalued; + bool show_revalued_only; + bool download_quotes; + + config_t(); +}; + +extern std::auto_ptr<config_t> config; + +void option_help(std::ostream& out); + +} // namespace ledger + +#endif // _CONFIG_H @@ -2,14 +2,26 @@ #ifdef DEBUG_ENABLED +#include <fstream> +#include <cstdlib> + namespace ledger { std::ostream * debug_stream = &std::cerr; bool free_debug_stream = false; -static class free_streams -{ - public: +static struct init_streams { + init_streams() { + // If debugging is enabled and DEBUG_FILE is set, all debugging + // output goes to that file. + if (const char * p = std::getenv("DEBUG_FILE")) { + debug_stream = new std::ofstream(p); + free_debug_stream = true; + } + } +} _debug_init; + +static struct free_streams { ~free_streams() { if (free_debug_stream && debug_stream) { delete debug_stream; @@ -177,7 +177,9 @@ entry_t * journal_t::derive_entry(strings_list::iterator i, return added; } -int parse_journal_file(const std::string& path, journal_t * journal) +int parse_journal_file(const std::string& path, + journal_t * journal, + account_t * master) { journal->sources.push_back(path); @@ -191,9 +193,11 @@ int parse_journal_file(const std::string& path, journal_t * journal) stream.seekg(0); if (magic == binary_magic_number) - return read_binary_journal(stream, journal, journal->master); + return read_binary_journal(stream, journal, + master ? master : journal->master); else - return parse_textual_journal(stream, journal, journal->master); + return parse_textual_journal(stream, journal, + master ? master : journal->master); } } // namespace ledger @@ -216,7 +216,9 @@ class journal_t strings_list::iterator end) const; }; -int parse_journal_file(const std::string& path, journal_t * journal); +int parse_journal_file(const std::string& path, + journal_t * journal, + account_t * master = NULL); unsigned int parse_textual_journal(std::istream& in, journal_t * ledger, @@ -3,483 +3,54 @@ #include "valexpr.h" #include "format.h" #include "walk.h" +#include "quotes.h" #include "option.h" +#include "config.h" #include "timing.h" +using namespace ledger; + +#include <iostream> #include <fstream> +#include <memory> +#include <algorithm> +#include <string> +#include <cstdlib> #include <cstring> -#include <unistd.h> #include <ctime> namespace { - -using namespace ledger; - -const std::string bal_fmt = "%20T %2_%-n\n"; -const std::string reg_fmt - = "%D %-.20P %-.22N %12.66t %12.80T\n\ -%/ %-.22N %12.66t %12.80T\n"; -const std::string plot_value_fmt = "%D %t\n"; -const std::string plot_total_fmt = "%D %T\n"; -const std::string print_fmt - = "\n%D %X%C%P\n %-34N %12o\n%/ %-34N %12o\n"; -const std::string equity_fmt - = "\n%D %X%C%P\n%/ %-34N %12t\n"; - -std::auto_ptr<journal_t> journal(new journal_t); -std::list<std::string> files; -std::auto_ptr<value_expr_t> sort_order; -std::auto_ptr<std::ostream> output_stream; -std::auto_ptr<interval_t> report_interval; - -#define OUT() (output_stream.get() ? *output_stream.get() : std::cout) - -std::string init_file; -std::string cache_file; -std::string price_db; -std::string predicate; -std::string display_predicate; -std::string format_string; -std::string sort_string; -std::string value_expr = "a"; -std::string total_expr = "T"; -std::time_t interval_begin = 0; -unsigned long pricing_leeway = 24 * 3600; - -bool cache_dirty = true; -bool show_subtotals = true; -bool show_expanded = false; -bool show_related = false; -bool show_all_related = false; -bool show_inverted = false; -bool show_empty = false; -bool days_of_the_week = false; -bool show_revalued = false; -bool show_revalued_only = false; - -void download_price_quote(commodity_t * commodity, - const std::time_t age, - const amount_t& price, - const std::time_t moment) -{ - std::time_t now = std::time(NULL); // the time of the query - - if (! (commodity->flags & COMMODITY_STYLE_CONSULTED) && - std::difftime(now, moment) < pricing_leeway && - (! price || std::difftime(moment, age) > pricing_leeway)) { - using namespace std; - - // Only consult the Internet once for any commodity - commodity->flags |= COMMODITY_STYLE_CONSULTED; - cache_dirty = true; - - char buf[256]; - buf[0] = '\0'; - - if (FILE * fp = popen((string("getquote ") + - commodity->symbol).c_str(), "r")) { - if (feof(fp) || ! fgets(buf, 255, fp)) { - fclose(fp); - return; - } - fclose(fp); - } - - if (buf[0]) { - char * p = strchr(buf, '\n'); - if (p) *p = '\0'; - - amount_t current; - current.parse(buf); - - commodity->add_price(now, current); - - if (! price_db.empty()) { - char buf[128]; - strftime(buf, 127, "%Y/%m/%d %H:%M:%S", localtime(&now)); - ofstream database(price_db.c_str(), ios_base::out | ios_base::app); - database << "P " << buf << " " << commodity->symbol << " " - << current << endl; - } - } - } -} - -static void show_version(std::ostream& out) -{ - out - << "Ledger " << ledger::version << ", the command-line accounting tool\n\n" - << "Copyright (c) 2003-2004, New Artisans LLC. All rights reserved.\n\n" - << "This program is made available under the terms of the BSD Public\n" - << "License. See the LICENSE file included with the distribution for\n" - << "details and disclaimer.\n"; + bool cache_dirty = false; + + TIMER_DEF(write_cache, "writing cache file"); + TIMER_DEF(report_gen, "generation of final report"); + TIMER_DEF(handle_options, "configuring based on options"); + TIMER_DEF(parse_files, "parsing ledger files"); + TIMER_DEF(process_env, "processing environment"); + TIMER_DEF(process_args, "processing command-line arguments"); + TIMER_DEF(read_cache, "reading cache file"); } -static void show_help(std::ostream& out) -{ - out - << "usage: ledger [options] COMMAND [options] [REGEXPS]\n\n\ -Basic options:\n\ - -h, --help display this help text\n\ - -v, --version display version information\n\ - -i, --init FILE initialize ledger by loading FILE\n\ - -f, --file FILE specify pathname of ledger data file\n\ - -o, --output FILE write all output to FILE\n\ - -p, --set-price CONV specifies commodity conversion: COMM=AMOUNT\n\n\ -Report filtering:\n\ - -b, --begin-date DATE specify a beginning date\n\ - -e, --end-date DATE specify an ending date\n\ - -c, --current do not show future entries (same as -e TODAY)\n\ - -C, --cleared show only cleared transactions and balances\n\ - -U, --uncleared show only uncleared transactions and balances\n\ - -R, --real do not consider virtual transactions: real only\n\n\ -Output customization:\n\ - -F, --format STR \n\ - -y, --date-format STR \n\ - -E, --empty balance: also show accounts that total to zero\n\ - -n, --collapse balance: no parent account totals; register: collapse\n\ - -s, --show-all balance: show sub-accounts; register: show subtotals\n\ - -S, --sort EXPR sort report according to value EXPR\n\ - -r, --related \n\ - -z, --interval EXPR \n\ - -w, --dow print register using day of week sub-totals\n\ - -W, --weekly \" \" weekly sub-totals\n\ - -M, --monthly \" \" monthly sub-totals\n\ - -Y, --yearly \" \" yearly sub-totals\n\ - -l, --limit EXPR don't calculate entries for which EXPR yields 0\n\ - -d, --display EXPR don't print entries for which EXPR yields 0\n\ - -t, --value EXPR \n\ - -T, --total EXPR \n\ - -j, --value-data \n\ - -J, --total-data \n\n\ -Commodity reporting:\n\ - -P, --price-db FILE sets the price database\n\ - -L, --price-exp MINS with -Q, fetch quotes only if data is older than MINS\n\ - -Q, --download download price information from the Internet\n\ - (works by running \"getquote SYMBOL\")\n\ - -O, --quantity \n\ - -B, --basis report cost basis of commodities\n\ - -V, --market report the market value of commodities\n\ - -G, --gain \n\ - -A, --average \n\ - -D, --deviation \n\ - -X, --trend \n\ - -Z, --weighted-trend \n\n\ -Commands:\n\ - balance show balance totals\n\ - register display a register for ACCOUNT\n\ - print print all ledger entries\n\ - entry output a newly formed entry, based on arguments\n\ - equity output equity entries for specified accounts\n"; -} - - -////////////////////////////////////////////////////////////////////// -// -// Basic options - -DEF_OPT_HANDLERS(); - -OPT_BEGIN(help, "h", false) { - show_help(std::cout); - std::exit(0); -} OPT_END(help); - -OPT_BEGIN(version, "v", false) { - show_version(std::cout); - std::exit(0); -} OPT_END(version); - -OPT_BEGIN(init, "i:", true) { - init_file = optarg; -} OPT_END(init); - -OPT_BEGIN(file, "f:", true) { - char * buf = new char[std::strlen(optarg) + 1]; - std::strcpy(buf, optarg); - for (char * p = std::strtok(buf, ":"); - p; - p = std::strtok(NULL, ":")) - files.push_back(p); - delete[] buf; -} OPT_END(file); - -OPT_BEGIN(cache, ":", false) { - cache_file = optarg; -} OPT_END(cache); - -OPT_BEGIN(output, "o:", false) { - if (std::string(optarg) != "-") - output_stream.reset(new std::ofstream(optarg)); -} OPT_END(output); - -OPT_BEGIN(set_price, "p:", true) { - // jww (2004-08-14): fix this relative to the other file settings - if (char * p = std::strchr(optarg, '=')) { - *p = ' '; - std::string conversion = "C "; - conversion += p; - std::istringstream stream(conversion); - parse_textual_journal(stream, journal.get(), journal->master); - } else { - std::cerr << "Error: Invalid price setting: " << optarg - << std::endl; - std::exit(1); - } -} OPT_END(set_price); - -////////////////////////////////////////////////////////////////////// -// -// Report filtering - -OPT_BEGIN(begin_date, "b:", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "(d>=["; - predicate += optarg; - predicate += "])"; -} OPT_END(begin_date); - -OPT_BEGIN(end_date, "e:", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "(d<["; - predicate += optarg; - predicate += "])"; -} OPT_END(end_date); - -OPT_BEGIN(current, "c", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "(d<t)"; -} OPT_END(current); - -OPT_BEGIN(cleared, "C", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "X"; -} OPT_END(cleared); - -OPT_BEGIN(uncleared, "U", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "!X"; -} OPT_END(uncleared); - -OPT_BEGIN(real, "R", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "R"; -} OPT_END(real); - -////////////////////////////////////////////////////////////////////// -// -// Output customization - -OPT_BEGIN(format, "F:", false) { - format_string = optarg; -} OPT_END(format); - -OPT_BEGIN(date_format, "y:", false) { - format_t::date_format = optarg; -} OPT_END(date_format); - -OPT_BEGIN(empty, "E", false) { - show_empty = true; -} OPT_END(empty); - -OPT_BEGIN(collapse, "n", false) { - show_subtotals = false; -} OPT_END(collapse); - -OPT_BEGIN(show_all, "s", false) { - show_expanded = true; -} OPT_END(show_all); - -OPT_BEGIN(sort, "S:", false) { - sort_string = optarg; -} OPT_END(sort); - -OPT_BEGIN(related, "r", false) { - show_related = true; -} OPT_END(related); - -OPT_BEGIN(interval, "z:", false) { - std::string str(optarg); - std::istringstream stream(str); - report_interval.reset(interval_t::parse(stream)); - - if (! stream.eof()) { - std::string word; - stream >> word; - if (word == "from") { - stream >> word; - if (! parse_date(word.c_str(), &interval_begin)) - throw interval_expr_error("Could not parse 'from' date"); - } - } -} OPT_END(interval); - -OPT_BEGIN(weekly, "W", false) { - report_interval.reset(new interval_t(604800, 0, 0)); -} OPT_END(weekly); - -OPT_BEGIN(dow, "w", false) { - days_of_the_week = true; -} OPT_END(dow); - -OPT_BEGIN(monthly, "M", false) { - report_interval.reset(new interval_t(0, 1, 0)); -} OPT_END(monthly); - -OPT_BEGIN(yearly, "Y", false) { - report_interval.reset(new interval_t(0, 0, 1)); -} OPT_END(yearly); - -OPT_BEGIN(limit, "l:", false) { - if (! predicate.empty()) - predicate += "&"; - predicate += "("; - predicate += optarg; - predicate += ")"; -} OPT_END(limit); - -OPT_BEGIN(display, "d:", false) { - if (! display_predicate.empty()) - display_predicate += "&"; - display_predicate += "("; - display_predicate += optarg; - display_predicate += ")"; -} OPT_END(display); - -OPT_BEGIN(value, "t:", false) { - value_expr = optarg; -} OPT_END(value); - -OPT_BEGIN(total, "T:", false) { - total_expr = optarg; -} OPT_END(total); - -OPT_BEGIN(value_data, "j", false) { - value_expr = "S" + value_expr; - format_string = plot_value_fmt; -} OPT_END(value_data); - -OPT_BEGIN(total_data, "J", false) { - total_expr = "S" + total_expr; - format_string = plot_total_fmt; -} OPT_END(total_data); - -////////////////////////////////////////////////////////////////////// -// -// Commodity reporting - -OPT_BEGIN(price_db, "P:", false) { - price_db = optarg; -} OPT_END(price_db); - -OPT_BEGIN(price_exp, "L:", false) { - pricing_leeway = std::atol(optarg) * 60; -} OPT_END(price_exp); - -OPT_BEGIN(download, "Q", false) { - commodity_t::updater = download_price_quote; -} OPT_END(download); - -OPT_BEGIN(quantity, "O", false) { - value_expr = "a"; - total_expr = "T"; -} OPT_END(quantity); - -OPT_BEGIN(basis, "B", false) { - value_expr = "c"; - total_expr = "C"; -} OPT_END(basis); - -OPT_BEGIN(market, "V", false) { - show_revalued = true; - - value_expr = "v"; - total_expr = "V"; -} OPT_END(market); - -OPT_BEGIN(gain, "G", false) { - show_revalued = - show_revalued_only = true; - - value_expr = "a"; - total_expr = "G"; -} OPT_END(gain); - -OPT_BEGIN(average, "A", false) { - value_expr = "a"; - total_expr = "MT"; -} OPT_END(average); - -OPT_BEGIN(deviation, "D", false) { - value_expr = "a"; - total_expr = "DMT"; -} OPT_END(deviation); - -OPT_BEGIN(trend, "X", false) { - value_expr = "a"; - total_expr = "MDMT"; -} OPT_END(trend); - -OPT_BEGIN(weighted_trend, "Z", false) { - value_expr = "a"; - total_expr = "MD(MT/(1+(((t-d)/(30*86400))<0?0:((t-d)/(30*86400)))))"; -} OPT_END(weighted_trend); - - -TIMER_DEF(write_cache, "writing cache file"); -TIMER_DEF(report_gen, "generation of final report"); -TIMER_DEF(handle_options, "configuring based on options"); -TIMER_DEF(parse_files, "parsing ledger files"); -TIMER_DEF(process_env, "processing environment"); -TIMER_DEF(process_args, "processing command-line arguments"); -TIMER_DEF(read_cache, "reading cache file"); - -} // namespace - int main(int argc, char * argv[], char * envp[]) { -#ifdef DEBUG_ENABLED - // If debugging is enabled, and DEBUG_FILE is set, then all - // debugging output goes to that file. - - if (const char * p = std::getenv("DEBUG_FILE")) { - debug_stream = new std::ofstream(p); - free_debug_stream = true; - } -#endif - - // Initialize default paths - - if (const char * p = std::getenv("HOME")) - init_file = cache_file = price_db = p; - - init_file += "/.ledgerrc"; - cache_file += "/.ledger"; - price_db += "/.pricedb"; + std::auto_ptr<journal_t> journal(new journal_t); // Parse command-line arguments TIMER_START(process_args); - std::list<std::string> args; - + strings_list args; process_arguments(argc, argv, false, args); if (args.empty()) { - show_help(std::cerr); + option_help(std::cerr); return 1; } - std::list<std::string>::iterator arg = args.begin(); + strings_list::iterator arg = args.begin(); TIMER_STOP(process_args); - const bool use_cache = files.empty(); + const bool use_cache = config->files.empty(); // Process options from the environment @@ -503,18 +74,19 @@ int main(int argc, char * argv[], char * envp[]) int entry_count = 0; try { - if (! init_file.empty()) - if (parse_journal_file(init_file, journal.get())) + if (! config->init_file.empty()) + if (parse_journal_file(config->init_file, journal.get())) throw error("Entries not allowed in initialization file"); - if (use_cache && ! cache_file.empty()) { + if (use_cache && ! config->cache_file.empty()) { journal->sources.clear(); // remove init_file - entry_count += parse_journal_file(cache_file, journal.get()); + entry_count += parse_journal_file(config->cache_file, journal.get()); journal->sources.pop_front(); // remove cache_file - std::list<std::string> exceptions; + strings_list exceptions; std::set_difference(journal->sources.begin(), journal->sources.end(), - files.begin(), files.end(), exceptions.begin()); + config->files.begin(), config->files.end(), + exceptions.begin()); if (entry_count == 0 || exceptions.size() > 0) { journal.reset(new journal_t); @@ -525,13 +97,27 @@ int main(int argc, char * argv[], char * envp[]) } if (entry_count == 0) - for (std::list<std::string>::iterator i = files.begin(); - i != files.end(); i++) + for (strings_list::iterator i = config->files.begin(); + i != config->files.end(); + i++) entry_count += parse_journal_file(*i, journal.get()); - if (! price_db.empty()) - if (parse_journal_file(price_db, journal.get())) + if (! config->price_db.empty()) + if (parse_journal_file(config->price_db, journal.get())) throw error("Entries not allowed in price history file"); + + for (strings_list::iterator i = config->price_settings.begin(); + i != config->price_settings.end(); + i++) { + std::string conversion = "C "; + conversion += *i; + int i = conversion.find('='); + if (i != -1) { + conversion[i] = ' '; + std::istringstream stream(conversion); + parse_textual_journal(stream, journal.get(), journal->master); + } + } } catch (error& err) { std::cerr << "Fatal: " << err.what() << std::endl; @@ -539,8 +125,8 @@ int main(int argc, char * argv[], char * envp[]) } if (entry_count == 0) { - std::cerr << ("Please specify ledger file(s) using -f option " - "or LEDGER environment variable.") << std::endl; + std::cerr << "Please specify ledger file(s) using -f option " + << "or LEDGER environment variable." << std::endl; return 1; } @@ -577,51 +163,53 @@ int main(int argc, char * argv[], char * envp[]) // Treat the remaining command-line arguments as regular // expressions, used for refining report results. - std::list<std::string>::iterator i = args.begin(); + strings_list::iterator i = args.begin(); for (; i != args.end(); i++) if (*i == "--") break; - std::string pred = regexps_to_predicate(arg, i); + const std::string pred = regexps_to_predicate(arg, i); if (! pred.empty()) { - if (! predicate.empty()) - predicate += "&"; - predicate += pred; + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += pred; } if (i != args.end()) { - std::string pred = regexps_to_predicate(i, args.end(), false); + const std::string pred = regexps_to_predicate(i, args.end(), false); if (! pred.empty()) { - if (! predicate.empty()) - predicate += "&"; - predicate += pred; + if (! config->predicate.empty()) + config->predicate += "&"; + config->predicate += pred; } } } // Compile the predicates - if (display_predicate.empty()) { + if (config->display_predicate.empty()) { if (command == "b") { - if (! show_empty) - display_predicate = "T"; + if (! config->show_empty) + config->display_predicate = "T"; - if (! show_expanded && predicate.empty()) { - if (! display_predicate.empty()) - display_predicate += "&"; - display_predicate += "!n"; + if (! config->show_expanded && config->predicate.empty()) { + if (! config->display_predicate.empty()) + config->display_predicate += "&"; + config->display_predicate += "!n"; } } else if (command == "E") { - display_predicate = "a"; + config->display_predicate = "a"; } } // Compile the sorting criteria - if (! sort_string.empty()) { + std::auto_ptr<value_expr_t> sort_order; + + if (! config->sort_string.empty()) { try { - std::istringstream stream(sort_string); + std::istringstream stream(config->sort_string); sort_order.reset(parse_value_expr(stream)); if (stream.peek() != -1) { std::ostringstream err; @@ -642,7 +230,7 @@ int main(int argc, char * argv[], char * envp[]) // Setup the meaning of %t and %T, used in format strings try { - format_t::value_expr.reset(parse_value_expr(value_expr)); + format_t::value_expr.reset(parse_value_expr(config->value_expr)); } catch (const value_expr_error& err) { std::cerr << "Error in amount (-t) specifier: " << err.what() @@ -651,7 +239,7 @@ int main(int argc, char * argv[], char * envp[]) } try { - format_t::total_expr.reset(parse_value_expr(total_expr)); + format_t::total_expr.reset(parse_value_expr(config->total_expr)); } catch (const value_expr_error& err) { std::cerr << "Error in total (-T) specifier: " << err.what() @@ -661,16 +249,18 @@ int main(int argc, char * argv[], char * envp[]) // Configure some option depending on the report type + bool show_all_related = false; + if (command == "p" || command == "e") { - show_related = show_all_related = true; - show_expanded = true; + config->show_related = show_all_related = true; + config->show_expanded = true; } else if (command == "E") { - show_expanded = true; + config->show_expanded = true; } - else if (show_related) { + else if (config->show_related) { if (command == "r") - show_inverted = true; + config->show_inverted = true; else show_all_related = true; } @@ -678,8 +268,8 @@ int main(int argc, char * argv[], char * envp[]) // Compile the format strings const char * f; - if (! format_string.empty()) - f = format_string.c_str(); + if (! config->format_string.empty()) + f = config->format_string.c_str(); else if (command == "b") f = bal_fmt.c_str(); else if (command == "r") @@ -704,6 +294,44 @@ int main(int argc, char * argv[], char * envp[]) TIMER_STOP(handle_options); + // Setup a few local and global variables, depending on the config + // settings. + + std::auto_ptr<std::ostream> output_stream; + std::auto_ptr<interval_t> report_interval; + std::time_t interval_begin; + + if (config->download_quotes) + commodity_t::updater = new quotes_by_script(config->price_db, + config->pricing_leeway, + cache_dirty); + + if (! config->output_file.empty()) + output_stream.reset(new std::ofstream(config->output_file.c_str())); + +#define OUT() (output_stream.get() ? *output_stream.get() : std::cout) + + if (! config->interval_text.empty()) { + std::istringstream stream(config->interval_text); + report_interval.reset(interval_t::parse(stream)); + if (! stream.eof()) { + std::string word; + stream >> word; + if (word == "from") { + stream >> word; + if (! parse_date(word.c_str(), &interval_begin)) { + std::cerr << "Error in report interval: " + << "Could not parse 'from' date" + << std::endl; + return 1; + } + } + } + } + + if (! config->date_format.empty()) + format_t::date_format = config->date_format; + // Walk the entries based on the report type and the options TIMER_START(report_gen); @@ -711,14 +339,15 @@ int main(int argc, char * argv[], char * envp[]) if (command == "b") { std::auto_ptr<item_handler<transaction_t> > formatter; formatter.reset(new add_to_account_value); - if (show_related) + if (config->show_related) formatter.reset(new related_transactions(formatter.release(), show_all_related)); - formatter.reset(new filter_transactions(formatter.release(), predicate)); + formatter.reset(new filter_transactions(formatter.release(), + config->predicate)); walk_entries(journal->entries, *formatter.get()); - format_account acct_formatter(OUT(), format, display_predicate); - if (show_subtotals) + format_account acct_formatter(OUT(), format, config->display_predicate); + if (config->show_subtotals) sum_accounts(journal->master); walk_accounts(journal->master, acct_formatter, sort_order.get()); @@ -731,10 +360,12 @@ int main(int argc, char * argv[], char * envp[]) else if (command == "E") { std::auto_ptr<item_handler<transaction_t> > formatter; formatter.reset(new add_to_account_value); - formatter.reset(new filter_transactions(formatter.release(), predicate)); + formatter.reset(new filter_transactions(formatter.release(), + config->predicate)); walk_entries(journal->entries, *formatter.get()); - format_equity acct_formatter(OUT(), format, nformat, display_predicate); + format_equity acct_formatter(OUT(), format, nformat, + config->display_predicate); sum_accounts(journal->master); walk_accounts(journal->master, acct_formatter, sort_order.get()); } @@ -762,24 +393,25 @@ int main(int argc, char * argv[], char * envp[]) // filter_transactions will only pass through transactions // matching the `display_predicate'. formatter.reset(new filter_transactions(formatter.release(), - display_predicate)); + config->display_predicate)); // calc_transactions computes the running total. When this // appears will determine, for example, whether filtered // transactions are included or excluded from the running total. - formatter.reset(new calc_transactions(formatter.release(), show_inverted)); + formatter.reset(new calc_transactions(formatter.release(), + config->show_inverted)); // changed_value_transactions adds virtual transactions to the // list to account for changes in market value of commodities, // which otherwise would affect the running total unpredictably. - if (show_revalued) + if (config->show_revalued) formatter.reset(new changed_value_transactions(formatter.release(), - show_revalued_only)); + config->show_revalued_only)); // collapse_transactions causes entries with multiple transactions // to appear as entries with a subtotaled transaction for each // commodity used. - if (! show_subtotals) + if (! config->show_subtotals) formatter.reset(new collapse_transactions(formatter.release())); // subtotal_transactions combines all the transactions it receives @@ -793,13 +425,13 @@ int main(int argc, char * argv[], char * envp[]) // dow_transactions is like interval_transactions, except that it // reports all the transactions that fall on each subsequent day // of the week. - if (show_expanded) + if (config->show_expanded) formatter.reset(new subtotal_transactions(formatter.release())); else if (report_interval.get()) formatter.reset(new interval_transactions(formatter.release(), *report_interval.get(), interval_begin)); - else if (days_of_the_week) + else if (config->days_of_the_week) formatter.reset(new dow_transactions(formatter.release())); // related_transactions will pass along all transactions related @@ -807,18 +439,21 @@ int main(int argc, char * argv[], char * envp[]) // then all the entry's transactions are passed; meaning that if // one transaction of an entry is to be printed, all the // transaction for that entry will be printed. - if (show_related) + if (config->show_related) formatter.reset(new related_transactions(formatter.release(), show_all_related)); // This filter_transactions will only pass through transactions // matching the `predicate'. - formatter.reset(new filter_transactions(formatter.release(), predicate)); + formatter.reset(new filter_transactions(formatter.release(), + config->predicate)); // Once the filters are chained, walk `journal's entries and start // feeding each transaction that matches `predicate' to the chain. walk_entries(journal->entries, *formatter.get()); + formatter->flush(); + #ifdef DEBUG_ENABLED // The transaction display flags (dflags) are not recorded in the // binary cache, and only need to be cleared if the transactions @@ -834,8 +469,8 @@ int main(int argc, char * argv[], char * envp[]) TIMER_START(write_cache); - if (use_cache && cache_dirty && ! cache_file.empty()) { - std::ofstream stream(cache_file.c_str()); + if (use_cache && cache_dirty && ! config->cache_file.empty()) { + std::ofstream stream(config->cache_file.c_str()); write_binary_journal(stream, journal.get(), &journal->sources); } diff --git a/quotes.cc b/quotes.cc new file mode 100644 index 00000000..4a21e9cb --- /dev/null +++ b/quotes.cc @@ -0,0 +1,55 @@ +#include "quotes.h" + +#include <fstream> + +namespace ledger { + +void quotes_by_script::operator()(commodity_t * commodity, + const std::time_t date, + const amount_t& price, + const std::time_t moment) +{ + std::time_t now = std::time(NULL); // the time of the query + + if (! (commodity->flags & COMMODITY_STYLE_CONSULTED) && + std::difftime(now, moment) < pricing_leeway && + (! price || std::difftime(moment, date) > pricing_leeway)) { + using namespace std; + + // Only consult the Internet once for any commodity + commodity->flags |= COMMODITY_STYLE_CONSULTED; + cache_dirty = true; + + char buf[256]; + buf[0] = '\0'; + + if (FILE * fp = popen((string("getquote ") + + commodity->symbol).c_str(), "r")) { + if (feof(fp) || ! fgets(buf, 255, fp)) { + fclose(fp); + return; + } + fclose(fp); + } + + if (buf[0]) { + char * p = strchr(buf, '\n'); + if (p) *p = '\0'; + + amount_t current; + current.parse(buf); + + commodity->add_price(now, current); + + if (! price_db.empty()) { + char buf[128]; + strftime(buf, 127, "%Y/%m/%d %H:%M:%S", localtime(&now)); + ofstream database(price_db.c_str(), ios_base::out | ios_base::app); + database << "P " << buf << " " << commodity->symbol << " " + << current << endl; + } + } + } +} + +} // namespace ledger diff --git a/quotes.h b/quotes.h new file mode 100644 index 00000000..8faa601a --- /dev/null +++ b/quotes.h @@ -0,0 +1,29 @@ +#ifndef _QUOTES_H +#define _QUOTES_H + +#include "amount.h" + +namespace ledger { + +class quotes_by_script : public commodity_t::updater_t +{ + std::string price_db; + unsigned long pricing_leeway; + bool& cache_dirty; + + public: + quotes_by_script(std::string _price_db, + unsigned long _pricing_leeway, + bool& _cache_dirty) + : price_db(_price_db), pricing_leeway(_pricing_leeway), + cache_dirty(_cache_dirty) {} + + virtual void operator()(commodity_t * commodity, + const std::time_t date, + const amount_t& price, + const std::time_t moment); +}; + +} // namespace ledger + +#endif // _QUOTES_H diff --git a/scripts/confirm.py b/scripts/confirm.py index ea39e94b..1132ca0c 100755 --- a/scripts/confirm.py +++ b/scripts/confirm.py @@ -15,7 +15,7 @@ last_line = "" for line in os.popen("../ledger %s reg %s" % (sys.argv[1], sys.argv[2])): value = clean(line[55:67]) - total = clean(line[68:80]) + total = clean(line[68:]) running_total += value if abs(running_total - total) > 0.001: @@ -223,10 +223,12 @@ bool finalize_entry(entry_t * entry) return ! balance; } -TIMER_DEF(entry_finish, "finalizing entry"); -TIMER_DEF(entry_xacts, "parsing transactions"); -TIMER_DEF(entry_details, "parsing entry details"); -TIMER_DEF(entry_date, "parsing entry date"); +namespace { + TIMER_DEF(entry_finish, "finalizing entry"); + TIMER_DEF(entry_xacts, "parsing transactions"); + TIMER_DEF(entry_details, "parsing entry details"); + TIMER_DEF(entry_date, "parsing entry date"); +} entry_t * parse_entry(std::istream& in, account_t * master) { @@ -543,7 +545,8 @@ unsigned int parse_textual_journal(std::istream& in, journal_t * journal, push_var<unsigned int> save_linenum(linenum); push_var<std::string> save_path(path); - count += parse_journal_file(skip_ws(line), journal); + count += parse_journal_file(skip_ws(line), journal, + account_stack.front()); } break; @@ -15,7 +15,7 @@ void sort_transactions::flush() transactions.clear(); - handler->flush(); + item_handler<transaction_t>::flush(); } void calc_transactions::operator()(transaction_t * xact) @@ -166,6 +166,8 @@ void subtotal_transactions::flush(const char * spec_fmt) } balances.clear(); + + item_handler<transaction_t>::flush(); } void subtotal_transactions::operator()(transaction_t * xact) @@ -22,12 +22,14 @@ struct item_handler { virtual void close() { flush(); if (handler) { - handler->flush(); delete handler; handler = NULL; } } - virtual void flush() {} + virtual void flush() { + if (handler) + handler->flush(); + } virtual void operator()(T * item) = 0; }; @@ -188,9 +190,7 @@ class collapse_transactions : public item_handler<transaction_t> virtual ~collapse_transactions() { close(); - delete totals_account; - for (transactions_deque::iterator i = xact_temps.begin(); i != xact_temps.end(); i++) @@ -200,6 +200,7 @@ class collapse_transactions : public item_handler<transaction_t> virtual void flush() { if (subtotal) report_cumulative_subtotal(); + item_handler<transaction_t>::flush(); } void report_cumulative_subtotal(); @@ -251,6 +252,7 @@ class changed_value_transactions : public item_handler<transaction_t> virtual void flush() { (*this)(NULL); + item_handler<transaction_t>::flush(); } virtual void operator()(transaction_t * xact); @@ -286,7 +288,11 @@ class subtotal_transactions : public item_handler<transaction_t> delete *i; } - virtual void flush(const char * spec_fmt = NULL); + void flush(const char * spec_fmt); + + virtual void flush() { + flush(NULL); + } virtual void operator()(transaction_t * xact); }; @@ -320,7 +326,6 @@ class dow_transactions : public subtotal_transactions : subtotal_transactions(handler) {} virtual void flush(); - virtual void operator()(transaction_t * xact) { struct std::tm * desc = std::gmtime(&xact->entry->date); days_of_the_week[desc->tm_wday].push_back(xact); |