/* * Copyright (c) 2003-2023, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of New Artisans LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include "select.h" #include "journal.h" #include "account.h" #include "report.h" #include "output.h" #include "print.h" #include "chain.h" #include "filters.h" #include "scope.h" #include "op.h" namespace ledger { namespace { bool get_principal_identifiers(expr_t::ptr_op_t expr, string& ident, bool do_transforms = false) { bool result = true; if (expr->is_ident()) { string name(expr->as_ident()); if (name == "date" || name == "aux_date" || name == "payee") { if (! ident.empty() && ! (name == "date" || name == "aux_date" || name == "payee")) result = false; ident = name; } else if (name == "account") { if (! ident.empty() && ! (name == "account")) result = false; ident = name; if (do_transforms) expr->set_ident("display_account"); } else if (name == "amount") { if (! ident.empty() && ! (name == "amount")) result = false; ident = name; if (do_transforms) expr->set_ident("display_amount"); } else if (name == "total") { if (! ident.empty() && ! (name == "total")) result = false; ident = name; if (do_transforms) expr->set_ident("display_total"); } } if (expr->kind > expr_t::op_t::TERMINALS || expr->is_scope()) { if (expr->left()) { if (! get_principal_identifiers(expr->left(), ident, do_transforms)) result = false; if (expr->kind > expr_t::op_t::UNARY_OPERATORS && expr->has_right()) if (! get_principal_identifiers(expr->right(), ident, do_transforms)) result = false; } } return result; } } value_t select_command(call_scope_t& args) { string text = "select " + join_args(args); if (text.empty()) throw std::logic_error(_("Usage: select TEXT")); report_t& report(find_scope(args)); // Our first step is to divide the select statement into its principal // parts: // // SELECT // FROM // WHERE // DISPLAY // COLLECT // GROUP BY // STYLE boost::regex select_re ("(select|from|where|display|collect|group\\s+by|style)\\s+" "(.+?)" "(?=(\\s+(from|where|display|collect|group\\s+by|style)\\s+|$))", boost::regex::perl | boost::regex::icase); boost::regex from_accounts_re("from\\s+accounts\\>"); bool accounts_report = boost::regex_search(text, from_accounts_re); boost::sregex_iterator m1(text.begin(), text.end(), select_re); boost::sregex_iterator m2; expr_t::ptr_op_t report_functor; std::ostringstream formatter; while (m1 != m2) { const boost::match_results& match(*m1); string keyword(match[1]); string arg(match[2]); DEBUG("select.parse", "keyword: " << keyword); DEBUG("select.parse", "arg: " << arg); if (keyword == "select") { expr_t args_expr(arg); value_t columns(split_cons_expr(args_expr.get_op())); bool first = true; string thus_far = ""; std::size_t cols = 0; #if HAVE_IOCTL struct winsize ws; #endif if (report.HANDLED(columns_)) cols = lexical_cast(report.HANDLER(columns_).value); else if (const char * columns_env = std::getenv("COLUMNS")) cols = lexical_cast(columns_env); #if HAVE_IOCTL else if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) != -1) cols = ws.ws_col; #endif else cols = 80; std::size_t date_width = (report.HANDLED(date_width_) ? lexical_cast(report.HANDLER(date_width_).str()) : static_cast (format_date(CURRENT_DATE(),FMT_PRINTED).length())); std::size_t payee_width = (report.HANDLED(payee_width_) ? lexical_cast(report.HANDLER(payee_width_).str()) : std::size_t(double(cols) * 0.263157)); std::size_t account_width = (report.HANDLED(account_width_) ? lexical_cast(report.HANDLER(account_width_).str()) : std::size_t(double(cols) * 0.302631)); std::size_t amount_width = (report.HANDLED(amount_width_) ? lexical_cast(report.HANDLER(amount_width_).str()) : std::size_t(double(cols) * 0.157894)); std::size_t total_width = (report.HANDLED(total_width_) ? lexical_cast(report.HANDLER(total_width_).str()) : amount_width); std::size_t meta_width = (report.HANDLED(meta_width_) ? lexical_cast(report.HANDLER(meta_width_).str()) : 10); bool saw_payee = false; bool saw_account = false; std::size_t cols_needed = 0; foreach (const value_t& column, columns.to_sequence()) { string ident; if (get_principal_identifiers(as_expr(column), ident)) { if (ident == "date" || ident == "aux_date") { cols_needed += date_width + 1; } else if (ident == "payee") { cols_needed += payee_width + 1; saw_payee = true; } else if (ident == "account") { cols_needed += account_width + 1; saw_account = true; } else if (ident == "amount") { cols_needed += amount_width + 1; } else if (ident == "total") { cols_needed += total_width + 1; } else { cols_needed += meta_width + 1; } } } while ((saw_account || saw_payee) && cols_needed < cols) { if (saw_account && cols_needed < cols) { ++account_width; ++cols_needed; if (cols_needed < cols) { ++account_width; ++cols_needed; } } if (saw_payee && cols_needed < cols) { ++payee_width; ++cols_needed; } } while ((saw_account || saw_payee) && cols_needed > cols && account_width > 5 && payee_width > 5) { DEBUG("auto.columns", "adjusting account down"); if (saw_account && cols_needed > cols) { --account_width; --cols_needed; if (cols_needed > cols) { --account_width; --cols_needed; } } if (saw_payee && cols_needed > cols) { --payee_width; --cols_needed; } DEBUG("auto.columns", "account_width now = " << account_width); } if (! report.HANDLED(date_width_)) report.HANDLER(date_width_).value = to_string(date_width); if (! report.HANDLED(payee_width_)) report.HANDLER(payee_width_).value = to_string(payee_width); if (! report.HANDLED(account_width_)) report.HANDLER(account_width_).value = to_string(account_width); if (! report.HANDLED(amount_width_)) report.HANDLER(amount_width_).value = to_string(amount_width); if (! report.HANDLED(total_width_)) report.HANDLER(total_width_).value = to_string(total_width); foreach (const value_t& column, columns.to_sequence()) { if (first) first = false; else formatter << ' '; formatter << "%("; string ident; if (get_principal_identifiers(as_expr(column), ident, true)) { if (ident == "date" || ident == "aux_date") { formatter << "ansify_if(" << "ansify_if(justify(format_date("; as_expr(column)->print(formatter); formatter << "), int(date_width)),"; formatter << "green if color and date > today)," << "bold if should_bold)"; if (! thus_far.empty()) thus_far += " + "; thus_far += "int(date_width) + 1"; } else if (ident == "payee") { formatter << "ansify_if(" << "ansify_if(justify(truncated("; as_expr(column)->print(formatter); formatter << ", int(payee_width)), int(payee_width)),"; formatter << "bold if color and !cleared and actual)," << "bold if should_bold)"; if (! thus_far.empty()) thus_far += " + "; thus_far += "int(payee_width) + 1"; } else if (ident == "account") { formatter << "ansify_if("; if (accounts_report) { formatter << "ansify_if("; formatter << "partial_account(options.flat), blue if color),"; } else { formatter << "justify(truncated("; as_expr(column)->print(formatter); formatter << ", int(account_width), int(abbrev_len))," << "int(account_width), -1, "; formatter << "false, color),"; if (! thus_far.empty()) thus_far += " + "; thus_far += "int(account_width) + 1"; } formatter << " bold if should_bold)"; } else if (ident == "amount" || ident == "total") { formatter << "ansify_if(" << "justify(scrub("; as_expr(column)->print(formatter); formatter << "), "; if (ident == "amount") formatter << "int(amount_width),"; else formatter << "int(total_width),"; if (! thus_far.empty()) thus_far += " + "; if (ident == "amount") thus_far += "int(amount_width)"; else thus_far += "int(total_width)"; if (thus_far.empty()) formatter << "-1"; else formatter << thus_far; formatter << ", true, color)," << " bold if should_bold)"; thus_far += " + 1"; } else { formatter << "ansify_if(" << "justify(truncated("; as_expr(column)->print(formatter); formatter << ", int(meta_width or 10)), int(meta_width) or 10),"; formatter << "bold if should_bold)"; if (! thus_far.empty()) thus_far += " + "; thus_far += "(int(meta_width) or 10) + 1"; } } formatter << ")"; } formatter << "\\n"; DEBUG("select.parse", "formatter: " << formatter.str()); } else if (keyword == "from") { if (arg == "xacts" || arg == "txns" || arg == "transactions") { report_functor = expr_t::op_t::wrap_functor (reporter<>(post_handler_ptr(new print_xacts(report, report.HANDLED(raw))), report, string("#select"))); } else if (arg == "posts" || arg == "postings") { report_functor = expr_t::op_t::wrap_functor (reporter<>(post_handler_ptr(new format_posts(report, formatter.str())), report, string("#select"))); } else if (arg == "accounts") { report_functor = expr_t::op_t::wrap_functor (reporter (acct_handler_ptr(new format_accounts(report, formatter.str())), report, string("#select"))); } else if (arg == "commodities") { report_functor = expr_t::op_t::wrap_functor (reporter (post_handler_ptr(new format_posts(report, formatter.str())), report, string("#select"))); } } else if (keyword == "where") { #if 0 query_t query; keep_details_t keeper(true, true, true); expr_t::ptr_op_t expr = query.parse_args(string_value(arg).to_sequence(), keeper, false, true); report.HANDLER(limit_).on("#select", query.get_query(query_t::QUERY_LIMIT)); #else report.HANDLER(limit_).on("#select", arg); #endif } else if (keyword == "display") { report.HANDLER(display_).on("#select", arg); } else if (keyword == "collect") { report.HANDLER(amount_).on("#select", arg); } else if (keyword == "group by") { report.HANDLER(group_by_).on("#select", arg); } else if (keyword == "style") { if (arg == "csv") { } else if (arg == "xml") { } else if (arg == "json") { } else if (arg == "emacs") { } } ++m1; } if (! report_functor) { report_functor = expr_t::op_t::wrap_functor (reporter<>(post_handler_ptr(new format_posts(report, formatter.str())), report, string("#select"))); } call_scope_t call_args(report); return report_functor->as_function()(call_args); } } // namespace ledger