From 7bddcd676bc53c6caad7dd283be362fcd53d5721 Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Sun, 23 May 2010 15:24:02 -0600 Subject: Added --rounding option, which is off by default The purpose of this option is to add special "" postings, to ensure that a regiter's running total is *always* the sum of its postings. Within --rounding, these adjustment postings are missing, which was the behavior in Ledger 2.x. It can be orders of magnitude slower to turn it on for large reports with many commodities. --- src/chain.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/chain.cc') diff --git a/src/chain.cc b/src/chain.cc index 86f639ad..44133391 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -86,7 +86,8 @@ post_handler_ptr chain_post_handlers(report_t& report, report.HANDLED(unrealized))) handler.reset(new changed_value_posts(handler, report, for_accounts_report, - report.HANDLED(unrealized))); + report.HANDLED(unrealized), + report.HANDLED(rounding))); // calc_posts computes the running total. When this appears will determine, // for example, whether filtered posts are included or excluded from the -- cgit v1.2.3 From a41d33fba37460587f59ea0349ac4947a4de9f3c Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Sun, 30 May 2010 02:35:25 -0600 Subject: Option --rounding inverted to --no-rounding --- src/chain.cc | 2 +- src/report.cc | 2 +- src/report.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/chain.cc') diff --git a/src/chain.cc b/src/chain.cc index 44133391..1103fe42 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -87,7 +87,7 @@ post_handler_ptr chain_post_handlers(report_t& report, handler.reset(new changed_value_posts(handler, report, for_accounts_report, report.HANDLED(unrealized), - report.HANDLED(rounding))); + ! report.HANDLED(no_rounding))); // calc_posts computes the running total. When this appears will determine, // for example, whether filtered posts are included or excluded from the diff --git a/src/report.cc b/src/report.cc index 4dee4bb2..f3186e0b 100644 --- a/src/report.cc +++ b/src/report.cc @@ -916,6 +916,7 @@ option_t * report_t::lookup_option(const char * p) case 'n': OPT_CH(collapse); else OPT(no_color); + else OPT(no_rounding); else OPT(no_titles); else OPT(no_total); else OPT(now_); @@ -955,7 +956,6 @@ option_t * report_t::lookup_option(const char * p) else OPT(revalued); else OPT(revalued_only); else OPT(revalued_total_); - else OPT(rounding); break; case 's': OPT(sort_); diff --git a/src/report.h b/src/report.h index 104cfc73..78dc2165 100644 --- a/src/report.h +++ b/src/report.h @@ -267,6 +267,7 @@ public: HANDLER(market).report(out); HANDLER(meta_).report(out); HANDLER(monthly).report(out); + HANDLER(no_rounding).report(out); HANDLER(no_titles).report(out); HANDLER(no_total).report(out); HANDLER(now_).report(out); @@ -296,7 +297,6 @@ public: HANDLER(revalued).report(out); HANDLER(revalued_only).report(out); HANDLER(revalued_total_).report(out); - HANDLER(rounding).report(out); HANDLER(seed_).report(out); HANDLER(sort_).report(out); HANDLER(sort_all_).report(out); @@ -638,6 +638,7 @@ public: parent->HANDLER(color).off(); }); + OPTION(report_t, no_rounding); OPTION(report_t, no_titles); OPTION(report_t, no_total); @@ -821,7 +822,6 @@ public: set_expr(args[0].to_string(), args[1].to_string()); }); - OPTION(report_t, rounding); OPTION(report_t, seed_); OPTION_(report_t, sort_, DO_(args) { // -S -- cgit v1.2.3 From 647d4aac2fa474085d01f7ea1cebdc34fafd64a6 Mon Sep 17 00:00:00 2001 From: John Wiegley Date: Sun, 30 May 2010 02:28:58 -0600 Subject: New: --group-by=EXPR and --group-title-format=FMT The --group-by option allows for most reports to be split up into sections based on the varying value of EXPR. For example, to see register subtotals by payee, use: ledger reg --group-by=payee -s This works for separated balances too: ledger bal --group-by=payee Another interesting possibility is seeing a register of all the accounts affected by a related account: ledger reg -r --group-by=payee The option --group-title-format can be used to add a separator bar to the group titles. The option --no-titles can be used to drop titles altogether. --- src/chain.cc | 126 +++++++++++++++++++++++++----------------------- src/chain.h | 14 ++++++ src/output.cc | 55 +++++++++++++++++---- src/output.h | 16 +++++++ src/print.cc | 12 ++++- src/print.h | 10 ++++ src/report.cc | 150 +++++++++++++++++++++++++++++++++++++++++++++------------- src/report.h | 18 +++++++ 8 files changed, 298 insertions(+), 103 deletions(-) (limited to 'src/chain.cc') diff --git a/src/chain.cc b/src/chain.cc index 1103fe42..b8c2eb0a 100644 --- a/src/chain.cc +++ b/src/chain.cc @@ -39,6 +39,73 @@ namespace ledger { +post_handler_ptr chain_pre_post_handlers(report_t& report, + post_handler_ptr base_handler) +{ + post_handler_ptr handler(base_handler); + + // anonymize_posts removes all meaningful information from xact payee's and + // account names, for the sake of creating useful bug reports. + if (report.HANDLED(anon)) + handler.reset(new anonymize_posts(handler)); + + // This filter_posts will only pass through posts matching the `predicate'. + if (report.HANDLED(limit_)) { + DEBUG("report.predicate", + "Report predicate expression = " << report.HANDLER(limit_).str()); + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + // budget_posts takes a set of posts from a data file and uses them to + // generate "budget posts" which balance against the reported posts. + // + // forecast_posts is a lot like budget_posts, except that it adds xacts + // only for the future, and does not balance them against anything but the + // future balance. + + if (report.budget_flags != BUDGET_NO_BUDGET) { + budget_posts * budget_handler = new budget_posts(handler, + report.budget_flags); + budget_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(budget_handler); + + // Apply this before the budget handler, so that only matching posts are + // calculated toward the budget. The use of filter_posts above will + // further clean the results so that no automated posts that don't match + // the filter get reported. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + else if (report.HANDLED(forecast_while_)) { + forecast_posts * forecast_handler + = new forecast_posts(handler, + predicate_t(report.HANDLER(forecast_while_).str(), + report.what_to_keep()), + report, + report.HANDLED(forecast_years_) ? + static_cast + (report.HANDLER(forecast_years_).value.to_long()) : + 5UL); + forecast_handler->add_period_xacts(report.session.journal->period_xacts); + handler.reset(forecast_handler); + + // See above, under budget_posts. + if (report.HANDLED(limit_)) + handler.reset(new filter_posts + (handler, predicate_t(report.HANDLER(limit_).str(), + report.what_to_keep()), + report)); + } + + return handler; +} + post_handler_ptr chain_post_handlers(report_t& report, post_handler_ptr base_handler, bool for_accounts_report) @@ -189,65 +256,6 @@ post_handler_ptr chain_post_handlers(report_t& report, if (report.HANDLED(related)) handler.reset(new related_posts(handler, report.HANDLED(related_all))); - // anonymize_posts removes all meaningful information from xact payee's and - // account names, for the sake of creating useful bug reports. - if (report.HANDLED(anon)) - handler.reset(new anonymize_posts(handler)); - - // This filter_posts will only pass through posts matching the `predicate'. - if (report.HANDLED(limit_)) { - DEBUG("report.predicate", - "Report predicate expression = " << report.HANDLER(limit_).str()); - handler.reset(new filter_posts - (handler, predicate_t(report.HANDLER(limit_).str(), - report.what_to_keep()), - report)); - } - - // budget_posts takes a set of posts from a data file and uses them to - // generate "budget posts" which balance against the reported posts. - // - // forecast_posts is a lot like budget_posts, except that it adds xacts - // only for the future, and does not balance them against anything but the - // future balance. - - if (report.budget_flags != BUDGET_NO_BUDGET) { - budget_posts * budget_handler = new budget_posts(handler, - report.budget_flags); - budget_handler->add_period_xacts(report.session.journal->period_xacts); - handler.reset(budget_handler); - - // Apply this before the budget handler, so that only matching posts are - // calculated toward the budget. The use of filter_posts above will - // further clean the results so that no automated posts that don't match - // the filter get reported. - if (report.HANDLED(limit_)) - handler.reset(new filter_posts - (handler, predicate_t(report.HANDLER(limit_).str(), - report.what_to_keep()), - report)); - } - else if (report.HANDLED(forecast_while_)) { - forecast_posts * forecast_handler - = new forecast_posts(handler, - predicate_t(report.HANDLER(forecast_while_).str(), - report.what_to_keep()), - report, - report.HANDLED(forecast_years_) ? - static_cast - (report.HANDLER(forecast_years_).value.to_long()) : - 5UL); - forecast_handler->add_period_xacts(report.session.journal->period_xacts); - handler.reset(forecast_handler); - - // See above, under budget_posts. - if (report.HANDLED(limit_)) - handler.reset(new filter_posts - (handler, predicate_t(report.HANDLER(limit_).str(), - report.what_to_keep()), - report)); - } - return handler; } diff --git a/src/chain.h b/src/chain.h index 935f2935..59b04eb8 100644 --- a/src/chain.h +++ b/src/chain.h @@ -91,11 +91,25 @@ typedef shared_ptr > post_handler_ptr; typedef shared_ptr > acct_handler_ptr; class report_t; + +post_handler_ptr +chain_pre_post_handlers(report_t& report, + post_handler_ptr base_handler); + post_handler_ptr chain_post_handlers(report_t& report, post_handler_ptr base_handler, bool for_accounts_report = false); +inline post_handler_ptr +chain_handlers(report_t& report, + post_handler_ptr handler, + bool for_accounts_report = false) { + handler = chain_post_handlers(report, handler, for_accounts_report); + handler = chain_pre_post_handlers(report, handler); + return handler; +} + } // namespace ledger #endif // _CHAIN_H diff --git a/src/output.cc b/src/output.cc index 183d80b3..f697dee4 100644 --- a/src/output.cc +++ b/src/output.cc @@ -45,7 +45,7 @@ format_posts::format_posts(report_t& _report, const optional& _prepend_format, std::size_t _prepend_width) : report(_report), prepend_width(_prepend_width), - last_xact(NULL), last_post(NULL) + last_xact(NULL), last_post(NULL), first_report_title(true) { TRACE_CTOR(format_posts, "report&, const string&, bool"); @@ -78,12 +78,30 @@ void format_posts::flush() void format_posts::operator()(post_t& post) { - std::ostream& out(report.output_stream); - if (! post.has_xdata() || ! post.xdata().has_flags(POST_EXT_DISPLAYED)) { + std::ostream& out(report.output_stream); + bind_scope_t bound_scope(report, post); + if (! report_title.empty()) { + if (first_report_title) + first_report_title = false; + else + out << '\n'; + + value_scope_t val_scope(string_value(report_title)); + bind_scope_t inner_scope(bound_scope, val_scope); + + format_t group_title_format; + group_title_format + .parse_format(report.HANDLER(group_title_format_).str()); + + out << group_title_format(inner_scope); + + report_title = ""; + } + if (prepend_format) { out.width(prepend_width); out << prepend_format(bound_scope); @@ -113,7 +131,8 @@ format_accounts::format_accounts(report_t& _report, const string& format, const optional& _prepend_format, std::size_t _prepend_width) - : report(_report), prepend_width(_prepend_width), disp_pred() + : report(_report), prepend_width(_prepend_width), disp_pred(), + first_report_title(true) { TRACE_CTOR(format_accounts, "report&, const string&"); @@ -144,19 +163,37 @@ std::size_t format_accounts::post_account(account_t& account, const bool flat) if (account.xdata().has_flags(ACCOUNT_EXT_TO_DISPLAY) && ! account.xdata().has_flags(ACCOUNT_EXT_DISPLAYED)) { + std::ostream& out(report.output_stream); + DEBUG("account.display", "Displaying account: " << account.fullname()); account.xdata().add_flags(ACCOUNT_EXT_DISPLAYED); bind_scope_t bound_scope(report, account); + if (! report_title.empty()) { + if (first_report_title) + first_report_title = false; + else + out << '\n'; + + value_scope_t val_scope(string_value(report_title)); + bind_scope_t inner_scope(bound_scope, val_scope); + + format_t group_title_format; + group_title_format + .parse_format(report.HANDLER(group_title_format_).str()); + + out << group_title_format(inner_scope); + + report_title = ""; + } + if (prepend_format) { - static_cast(report.output_stream).width(prepend_width); - static_cast(report.output_stream) - << prepend_format(bound_scope); + out.width(prepend_width); + out << prepend_format(bound_scope); } - static_cast(report.output_stream) - << account_line_format(bound_scope); + out << account_line_format(bound_scope); return 1; } diff --git a/src/output.h b/src/output.h index f0e7f9a5..a19c6235 100644 --- a/src/output.h +++ b/src/output.h @@ -64,6 +64,8 @@ protected: std::size_t prepend_width; xact_t * last_xact; post_t * last_post; + bool first_report_title; + string report_title; public: format_posts(report_t& _report, const string& format, @@ -73,6 +75,10 @@ public: TRACE_DTOR(format_posts); } + virtual void title(const string& str) { + report_title = str; + } + virtual void flush(); virtual void operator()(post_t& post); @@ -80,6 +86,8 @@ public: last_xact = NULL; last_post = NULL; + report_title = ""; + item_handler::clear(); } }; @@ -94,6 +102,8 @@ protected: format_t prepend_format; std::size_t prepend_width; predicate_t disp_pred; + bool first_report_title; + string report_title; std::list posted_accounts; @@ -108,6 +118,10 @@ public: std::pair mark_accounts(account_t& account, const bool flat); + virtual void title(const string& str) { + report_title = str; + } + virtual std::size_t post_account(account_t& account, const bool flat); virtual void flush(); @@ -117,6 +131,8 @@ public: disp_pred.mark_uncompiled(); posted_accounts.clear(); + report_title = ""; + item_handler::clear(); } }; diff --git a/src/print.cc b/src/print.cc index 5c46f4e7..a8aa5872 100644 --- a/src/print.cc +++ b/src/print.cc @@ -42,7 +42,7 @@ namespace ledger { print_xacts::print_xacts(report_t& _report, bool _print_raw) - : report(_report), print_raw(_print_raw) + : report(_report), print_raw(_print_raw), first_title(true) { TRACE_CTOR(print_xacts, "report&, bool"); } @@ -221,6 +221,16 @@ namespace { } } +void print_xacts::title(const string&) +{ + if (first_title) { + first_title = false; + } else { + std::ostream& out(report.output_stream); + out << '\n'; + } +} + void print_xacts::flush() { std::ostream& out(report.output_stream); diff --git a/src/print.h b/src/print.h index f323b153..5263ec91 100644 --- a/src/print.h +++ b/src/print.h @@ -62,6 +62,7 @@ protected: xacts_present_map xacts_present; xacts_list xacts; bool print_raw; + bool first_title; public: print_xacts(report_t& _report, bool _print_raw = false); @@ -69,8 +70,17 @@ public: TRACE_DTOR(print_xacts); } + virtual void title(const string&); + virtual void flush(); virtual void operator()(post_t& post); + + virtual void clear() { + xacts_present.clear(); + xacts.clear(); + + item_handler::clear(); + } }; diff --git a/src/report.cc b/src/report.cc index f3186e0b..dcb56319 100644 --- a/src/report.cc +++ b/src/report.cc @@ -274,10 +274,35 @@ void report_t::parse_query_args(const value_t& args, const string& whence) } } +namespace { + struct posts_flusher + { + report_t& report; + post_handler_ptr handler; + + posts_flusher(report_t& _report, post_handler_ptr _handler) + : report(_report), handler(_handler) {} + + void operator()(const value_t&) { + report.session.journal->clear_xdata(); + } + }; +} + void report_t::posts_report(post_handler_ptr handler) { + handler = chain_post_handlers(*this, handler); + if (HANDLED(group_by_)) { + std::auto_ptr + splitter(new post_splitter(*this, handler, HANDLER(group_by_).expr)); + splitter->set_postflush_func(posts_flusher(*this, handler)); + handler = post_handler_ptr(splitter.release()); + } + handler = chain_pre_post_handlers(*this, handler); + journal_posts_iterator walker(*session.journal.get()); - pass_down_posts(chain_post_handlers(*this, handler), walker); + pass_down_posts(handler, walker); + session.journal->clear_xdata(); } @@ -285,66 +310,121 @@ void report_t::generate_report(post_handler_ptr handler) { HANDLER(limit_).on(string("#generate"), "actual"); + handler = chain_handlers(*this, handler); + generate_posts_iterator walker (session, HANDLED(seed_) ? static_cast(HANDLER(seed_).value.to_long()) : 0, HANDLED(head_) ? static_cast(HANDLER(head_).value.to_long()) : 50); - pass_down_posts(chain_post_handlers(*this, handler), walker); + pass_down_posts(handler, walker); } void report_t::xact_report(post_handler_ptr handler, xact_t& xact) { + handler = chain_handlers(*this, handler); + xact_posts_iterator walker(xact); - pass_down_posts(chain_post_handlers(*this, handler), walker); + pass_down_posts(handler, walker); + xact.clear_xdata(); } +namespace { + struct accounts_title_printer + { + report_t& report; + acct_handler_ptr handler; + + accounts_title_printer(report_t& _report, acct_handler_ptr _handler) + : report(_report), handler(_handler) {} + + void operator()(const value_t& val) + { + if (! report.HANDLED(no_titles)) { + std::ostringstream buf; + val.print(buf); + handler->title(buf.str()); + } + } + }; + + struct accounts_flusher + { + report_t& report; + acct_handler_ptr handler; + + accounts_flusher(report_t& _report, acct_handler_ptr _handler) + : report(_report), handler(_handler) {} + + void operator()(const value_t&) + { + report.HANDLER(amount_).expr.mark_uncompiled(); + report.HANDLER(total_).expr.mark_uncompiled(); + report.HANDLER(display_amount_).expr.mark_uncompiled(); + report.HANDLER(display_total_).expr.mark_uncompiled(); + report.HANDLER(revalued_total_).expr.mark_uncompiled(); + + scoped_ptr iter; + if (! report.HANDLED(sort_)) { + iter.reset(new basic_accounts_iterator(*report.session.journal->master)); + } else { + expr_t sort_expr(report.HANDLER(sort_).str()); + sort_expr.set_context(&report); + iter.reset(new sorted_accounts_iterator(*report.session.journal->master, + sort_expr, report.HANDLED(flat))); + } + + if (report.HANDLED(display_)) { + DEBUG("report.predicate", + "Display predicate = " << report.HANDLER(display_).str()); + pass_down_accounts(handler, *iter.get(), + predicate_t(report.HANDLER(display_).str(), + report.what_to_keep()), + report); + } else { + pass_down_accounts(handler, *iter.get()); + } + + report.session.journal->clear_xdata(); + } + }; +} + void report_t::accounts_report(acct_handler_ptr handler) { - journal_posts_iterator walker(*session.journal.get()); - - // The lifetime of the chain object controls the lifetime of all temporary - // objects created within it during the call to pass_down_posts, which will - // be needed later by the pass_down_accounts. post_handler_ptr chain = - chain_post_handlers(*this, post_handler_ptr(new ignore_posts), true); - pass_down_posts(chain, walker); + chain_post_handlers(*this, post_handler_ptr(new ignore_posts), + /* for_accounts_report= */ true); + if (HANDLED(group_by_)) { + std::auto_ptr + splitter(new post_splitter(*this, chain, HANDLER(group_by_).expr)); - HANDLER(amount_).expr.mark_uncompiled(); - HANDLER(total_).expr.mark_uncompiled(); - HANDLER(display_amount_).expr.mark_uncompiled(); - HANDLER(display_total_).expr.mark_uncompiled(); - HANDLER(revalued_total_).expr.mark_uncompiled(); + splitter->set_preflush_func(accounts_title_printer(*this, handler)); + splitter->set_postflush_func(accounts_flusher(*this, handler)); - scoped_ptr iter; - if (! HANDLED(sort_)) { - iter.reset(new basic_accounts_iterator(*session.journal->master)); - } else { - expr_t sort_expr(HANDLER(sort_).str()); - sort_expr.set_context(this); - iter.reset(new sorted_accounts_iterator(*session.journal->master, - sort_expr, HANDLED(flat))); + chain = post_handler_ptr(splitter.release()); } + chain = chain_pre_post_handlers(*this, chain); - if (HANDLED(display_)) { - DEBUG("report.predicate", - "Display predicate = " << HANDLER(display_).str()); - pass_down_accounts(handler, *iter.get(), - predicate_t(HANDLER(display_).str(), what_to_keep()), - *this); - } else { - pass_down_accounts(handler, *iter.get()); - } + // The lifetime of the chain object controls the lifetime of all temporary + // objects created within it during the call to pass_down_posts, which will + // be needed later by the pass_down_accounts. + journal_posts_iterator walker(*session.journal.get()); + pass_down_posts(chain, walker); - session.journal->clear_xdata(); + if (! HANDLED(group_by_)) + accounts_flusher(*this, handler)(value_t()); } void report_t::commodities_report(post_handler_ptr handler) { + handler = chain_handlers(*this, handler); + posts_commodities_iterator walker(*session.journal.get()); - pass_down_posts(chain_post_handlers(*this, handler), walker); + pass_down_posts(handler, walker); + session.journal->clear_xdata(); } @@ -888,6 +968,8 @@ option_t * report_t::lookup_option(const char * p) break; case 'g': OPT(gain); + else OPT(group_by_); + else OPT(group_title_format_); break; case 'h': OPT(head_); diff --git a/src/report.h b/src/report.h index 78dc2165..6fa238f0 100644 --- a/src/report.h +++ b/src/report.h @@ -256,6 +256,8 @@ public: HANDLER(forecast_years_).report(out); HANDLER(format_).report(out); HANDLER(gain).report(out); + HANDLER(group_by_).report(out); + HANDLER(group_title_format_).report(out); HANDLER(head_).report(out); HANDLER(invert).report(out); HANDLER(limit_).report(out); @@ -596,6 +598,22 @@ public: " - get_at(total_expr, 1)"); }); + OPTION__ + (report_t, group_by_, + expr_t expr; + CTOR(report_t, group_by_) {} + void set_expr(const optional& whence, const string& str) { + expr = str; + on(whence, str); + } + DO_(args) { + set_expr(args[0].to_string(), args[1].to_string()); + }); + + OPTION__(report_t, group_title_format_, CTOR(report_t, group_title_format_) { + on(none, "%(value)\n"); + }); + OPTION(report_t, head_); OPTION_(report_t, invert, DO() { -- cgit v1.2.3