#include "format.h" #include "error.h" namespace ledger { std::string truncated(const std::string& str, unsigned int width) { char buf[256]; std::memset(buf, '\0', 255); assert(width < 256); std::strncpy(buf, str.c_str(), str.length()); if (buf[width]) std::strcpy(&buf[width - 2], ".."); return buf; } std::string partial_account_name(const account_t * account, const unsigned int start_depth) { std::string name = account->name; const account_t * acct = account->parent; for (int i = account->depth - start_depth - 1; --i >= 0 && acct->parent; ) { assert(acct); name = acct->name + ":" + name; acct = acct->parent; } return name; } std::auto_ptr format_t::value_expr; std::auto_ptr format_t::total_expr; element_t * format_t::parse_elements(const std::string& fmt) { element_t * result = NULL; element_t * current = NULL; std::string str; for (const char * p = fmt.c_str(); *p; p++) { if (*p == '%') { if (! result) { current = result = new element_t; } else { current->next = new element_t; current = current->next; } if (! str.empty()) { current->type = element_t::STRING; current->chars = str; str = ""; current->next = new element_t; current = current->next; } ++p; if (*p == '-') { current->align_left = true; ++p; } std::string num; while (*p && std::isdigit(*p)) num += *p++; if (! num.empty()) current->min_width = std::atol(num.c_str()); if (*p == '.') { ++p; num = ""; while (*p && std::isdigit(*p)) num += *p++; if (! num.empty()) { current->max_width = std::atol(num.c_str()); if (current->min_width == 0) current->min_width = current->max_width; } } switch (*p) { case '%': current->type = element_t::STRING; current->chars = "%"; break; case '(': ++p; num = ""; while (*p && *p != ')') num += *p++; if (*p != ')') throw format_error("Missing ')'"); current->type = element_t::VALUE_EXPR; current->val_expr = parse_expr(num); break; case '[': ++p; num = ""; while (*p && *p != ']') num += *p++; if (*p != ']') throw format_error("Missing ']'"); current->type = element_t::DATE_STRING; current->chars = num; break; case 'd': current->type = element_t::DATE_STRING; current->chars = "%Y/%m/%d"; break; case 'X': current->type = element_t::CLEARED; break; case 'C': current->type = element_t::CODE; break; case 'p': current->type = element_t::PAYEE; break; case 'n': current->type = element_t::ACCOUNT_NAME; break; case 'N': current->type = element_t::ACCOUNT_FULLNAME; break; case 'o': current->type = element_t::OPT_AMOUNT; break; case 't': current->type = element_t::VALUE; break; case 'T': current->type = element_t::TOTAL; break; case '_': current->type = element_t::SPACER; break; } } else { str += *p; } } if (! str.empty()) { if (! result) { current = result = new element_t; } else { current->next = new element_t; current = current->next; } current->type = element_t::STRING; current->chars = str; } return result; } void format_t::format_elements(std::ostream& out, const details_t& details) const { for (const element_t * elem = elements; elem; elem = elem->next) { if (elem->align_left) out << std::left; else out << std::right; if (elem->min_width > 0) out.width(elem->min_width); switch (elem->type) { case element_t::STRING: out << elem->chars; break; case element_t::VALUE_EXPR: { balance_t value; elem->val_expr->compute(value, details); value.write(out, elem->min_width, (elem->max_width > 0 ? elem->max_width : elem->min_width)); break; } case element_t::DATE_STRING: if (details.entry && details.entry->date != -1) { char buf[256]; std::strftime(buf, 255, elem->chars.c_str(), std::gmtime(&details.entry->date)); out << (elem->max_width == 0 ? buf : truncated(buf, elem->max_width)); } else { out << " "; } break; case element_t::CLEARED: if (details.entry && details.entry->state == entry_t::CLEARED) out << "* "; else out << ""; break; case element_t::CODE: if (details.entry && ! details.entry->code.empty()) out << "(" << details.entry->code << ") "; else out << ""; break; case element_t::PAYEE: if (details.entry) out << (elem->max_width == 0 ? details.entry->payee : truncated(details.entry->payee, elem->max_width)); break; case element_t::ACCOUNT_NAME: case element_t::ACCOUNT_FULLNAME: if (details.account) { std::string name = (elem->type == element_t::ACCOUNT_FULLNAME ? details.account->fullname() : partial_account_name(details.account, details.depth)); if (elem->max_width > 0) name = truncated(name, elem->max_width); if (details.xact && details.xact->flags & TRANSACTION_VIRTUAL) { if (details.xact->flags & TRANSACTION_BALANCE) name = "[" + name + "]"; else name = "(" + name + ")"; } out << name; } else { out << " "; } break; case element_t::OPT_AMOUNT: { if (! details.entry || ! details.xact) break; std::string disp; bool use_disp = false; if (std::find(details.entry->transactions.begin(), details.entry->transactions.end(), details.xact) != details.entry->transactions.end()) { if (details.entry->transactions.size() == 2 && details.xact == details.entry->transactions.back() && (details.entry->transactions.front()->amount == details.entry->transactions.front()->cost) && (details.entry->transactions.front()->amount == - details.entry->transactions.back()->amount)) { use_disp = true; } else if (details.entry->transactions.size() != 2 && details.xact->amount != details.xact->cost) { amount_t unit_cost = details.xact->cost / details.xact->amount; std::ostringstream stream; stream << details.xact->amount << " @ " << unit_cost; disp = stream.str(); use_disp = true; } } if (! use_disp) disp = std::string(details.xact->amount); out << disp; // jww (2004-07-31): this should be handled differently if (! details.xact->note.empty()) out << " ; " << details.xact->note; break; } case element_t::VALUE: { balance_t value; compute_value(value, details); value.write(out, elem->min_width, (elem->max_width > 0 ? elem->max_width : elem->min_width)); break; } case element_t::TOTAL: { balance_t value; compute_total(value, details); value.write(out, elem->min_width, (elem->max_width > 0 ? elem->max_width : elem->min_width)); break; } case element_t::SPACER: for (unsigned int i = 0; i < details.depth; i++) { if (elem->min_width > 0 || elem->max_width > 0) out.width(elem->min_width > elem->max_width ? elem->min_width : elem->max_width); out << " "; } break; default: assert(0); break; } } } void format_transaction::report_cumulative_subtotal() const { if (count == 1) { if (! intercept || ! intercept(last_xact)) first_line_format.format_elements(output_stream, details_t(last_xact)); return; } assert(count > 1); account_t splits(NULL, ""); transaction_t splits_total(NULL, &splits); splits_total.total = subtotal; balance_t value; format_t::compute_total(value, details_t(&splits_total)); splits_total.entry = last_entry; splits_total.total = last_xact->total; bool first = true; for (amounts_map::const_iterator i = value.amounts.begin(); i != value.amounts.end(); i++) { splits_total.amount = (*i).second; splits_total.cost = (*i).second; splits_total.total += (*i).second; if (first) { if (! intercept || ! intercept(&splits_total)) first_line_format.format_elements(output_stream, details_t(&splits_total)); first = false; } else { next_lines_format.format_elements(output_stream, details_t(&splits_total)); } } } void format_transaction::operator()(transaction_t * xact) const { if (last_xact) xact->total = last_xact->total; if (inverted) { xact->amount.negate(); xact->cost.negate(); } xact->total += *xact; xact->index = last_xact ? last_xact->index + 1 : 0; if (! disp_pred_functor(xact)) return; xact->flags |= TRANSACTION_DISPLAYED; // This makes the assumption that transactions from a single entry // are always grouped together. if (collapsed) { // If we've reached a new entry, report on the subtotal // accumulated thus far. if (last_entry && last_entry != xact->entry) { report_cumulative_subtotal(); subtotal = 0; count = 0; } subtotal += *xact; count++; } else { if (last_entry != xact->entry) { if (! intercept || ! intercept(xact)) first_line_format.format_elements(output_stream, details_t(xact)); } else { next_lines_format.format_elements(output_stream, details_t(xact)); } } if (inverted) { xact->amount.negate(); xact->cost.negate(); } last_entry = xact->entry; last_xact = xact; } #if 0 bool report_changed_values(transaction_t * xact) { static transaction_t * last_xact = NULL; if (last_xact) { balance_t prev_bal, cur_bal; format_t::compute_total(prev_bal, details_t(last_xact)); format_t::compute_total(cur_bal, details_t(xact)); if (balance_t diff = cur_bal - prev_bal) { entry_t modified_entry; transaction_t new_xact(&modified_entry, NULL); modified_entry.date = xact ? xact->entry->date : std::time(NULL); modified_entry.payee = "Commodities revalued"; new_xact.amount = diff.amount(); format_t::compute_value(diff, details_t(xact)); new_xact.total = cur_bal - diff; functor(&new_xact); } } last_xact = xact; } #endif void format_account::operator()(const account_t * account, const unsigned int max_depth, const bool report_top) const { // Don't output the account if only one child will be displayed // which shows the exact same amount. jww (2004-08-03): How do // compute the right figure? It should a value expression specified // by the user, to say, "If this expression is equivalent between a // parent account and a lone displayed child, then don't display the // parent." if (bool output = report_top || account->parent != NULL) { int counted = 0; bool display = false; for (accounts_map::const_iterator i = account->accounts.begin(); i != account->accounts.end(); i++) { if (! (*i).second->total) continue; if ((*i).second->total != account->total || counted > 0) { display = true; break; } counted++; } if (counted == 1 && ! display) output = false; if (output) { unsigned int depth = account->depth; if (max_depth == 0 || depth <= max_depth) { for (const account_t * acct = account; depth > 0 && acct && acct != last_account; acct = acct->parent) depth--; format.format_elements(output_stream, details_t(account, depth)); last_account = account; } } } } } // namespace ledger