summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2010-05-30 02:28:58 -0600
committerJohn Wiegley <johnw@newartisans.com>2010-05-30 02:47:40 -0600
commit647d4aac2fa474085d01f7ea1cebdc34fafd64a6 (patch)
treec17157d73c3a9d5dd182a4b7b0896e4f31e318be /src
parenta41d33fba37460587f59ea0349ac4947a4de9f3c (diff)
downloadfork-ledger-647d4aac2fa474085d01f7ea1cebdc34fafd64a6.tar.gz
fork-ledger-647d4aac2fa474085d01f7ea1cebdc34fafd64a6.tar.bz2
fork-ledger-647d4aac2fa474085d01f7ea1cebdc34fafd64a6.zip
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.
Diffstat (limited to 'src')
-rw-r--r--src/chain.cc126
-rw-r--r--src/chain.h14
-rw-r--r--src/output.cc55
-rw-r--r--src/output.h16
-rw-r--r--src/print.cc12
-rw-r--r--src/print.h10
-rw-r--r--src/report.cc150
-rw-r--r--src/report.h18
8 files changed, 298 insertions, 103 deletions
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<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;
}
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<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
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<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 (! 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<std::ostream&>(report.output_stream).width(prepend_width);
- static_cast<std::ostream&>(report.output_stream)
- << prepend_format(bound_scope);
+ out.width(prepend_width);
+ out << prepend_format(bound_scope);
}
- static_cast<std::ostream&>(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<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();
}
};
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<post_t>::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<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());
-
- // 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<post_splitter>
+ 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<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)));
+ 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> * 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<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() {