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.
This commit is contained in:
John Wiegley 2010-05-30 02:28:58 -06:00
parent a41d33fba3
commit 647d4aac2f
8 changed files with 299 additions and 104 deletions

View file

@ -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<std::size_t>
(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<std::size_t>
(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;
}

View file

@ -91,11 +91,25 @@ typedef shared_ptr<item_handler<post_t> > post_handler_ptr;
typedef shared_ptr<item_handler<account_t> > 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

View file

@ -45,7 +45,7 @@ format_posts::format_posts(report_t& _report,
const optional<string>& _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<string>& _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 (prepend_format) {
static_cast<std::ostream&>(report.output_stream).width(prepend_width);
static_cast<std::ostream&>(report.output_stream)
<< prepend_format(bound_scope);
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 = "";
}
static_cast<std::ostream&>(report.output_stream)
<< account_line_format(bound_scope);
if (prepend_format) {
out.width(prepend_width);
out << prepend_format(bound_scope);
}
out << account_line_format(bound_scope);
return 1;
}

View file

@ -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<post_t>::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<account_t *> posted_accounts;
@ -108,6 +118,10 @@ public:
std::pair<std::size_t, std::size_t>
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<account_t>::clear();
}
};

View file

@ -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);

View file

@ -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<post_t>::clear();
}
};

View file

@ -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<post_splitter>
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<unsigned int>(HANDLER(seed_).value.to_long()) : 0,
HANDLED(head_) ?
static_cast<unsigned int>(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<accounts_iterator> 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());
post_handler_ptr chain =
chain_post_handlers(*this, post_handler_ptr(new ignore_posts),
/* for_accounts_report= */ true);
if (HANDLED(group_by_)) {
std::auto_ptr<post_splitter>
splitter(new post_splitter(*this, chain, HANDLER(group_by_).expr));
splitter->set_preflush_func(accounts_title_printer(*this, handler));
splitter->set_postflush_func(accounts_flusher(*this, handler));
chain = post_handler_ptr(splitter.release());
}
chain = chain_pre_post_handlers(*this, chain);
// 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);
journal_posts_iterator walker(*session.journal.get());
pass_down_posts(chain, walker);
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();
scoped_ptr<accounts_iterator> 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)));
}
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());
}
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> * 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_);

View file

@ -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<string>& 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() {