summaryrefslogtreecommitdiff
path: root/src/textual.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/textual.cc')
-rw-r--r--src/textual.cc1396
1 files changed, 1396 insertions, 0 deletions
diff --git a/src/textual.cc b/src/textual.cc
new file mode 100644
index 00000000..cf670cae
--- /dev/null
+++ b/src/textual.cc
@@ -0,0 +1,1396 @@
+/*
+ * Copyright (c) 2003-2009, 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 <system.hh>
+
+#include "journal.h"
+#include "xact.h"
+#include "post.h"
+#include "account.h"
+#include "option.h"
+#include "query.h"
+#include "pstream.h"
+#include "pool.h"
+#include "session.h"
+
+#define TIMELOG_SUPPORT 1
+#if defined(TIMELOG_SUPPORT)
+#include "timelog.h"
+#endif
+
+namespace ledger {
+
+namespace {
+ class instance_t : public noncopyable, public scope_t
+ {
+ static const std::size_t MAX_LINE = 1024;
+
+ public:
+ typedef std::pair<commodity_t *, amount_t> fixed_rate_t;
+ typedef variant<account_t *, string, fixed_rate_t> state_t;
+
+ std::list<state_t>& state_stack;
+
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& timelog;
+#endif
+ instance_t * parent;
+ std::istream& in;
+ scope_t& scope;
+ journal_t& journal;
+ account_t * master;
+ const path * original_file;
+ accounts_map account_aliases;
+ bool strict;
+ path pathname;
+ char linebuf[MAX_LINE + 1];
+ std::size_t linenum;
+ istream_pos_type line_beg_pos;
+ istream_pos_type curr_pos;
+ std::size_t count;
+ std::size_t errors;
+
+ optional<date_t::year_type> current_year;
+
+ instance_t(std::list<state_t>& _state_stack,
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& _timelog,
+#endif
+ std::istream& _in,
+ scope_t& _scope,
+ journal_t& _journal,
+ account_t * _master = NULL,
+ const path * _original_file = NULL,
+ bool _strict = false,
+ instance_t * _parent = NULL);
+
+ ~instance_t();
+
+ bool front_is_account() {
+ return state_stack.front().type() == typeid(account_t *);
+ }
+ bool front_is_string() {
+ return state_stack.front().type() == typeid(string);
+ }
+ bool front_is_fixed_rate() {
+ return state_stack.front().type() == typeid(fixed_rate_t);
+ }
+
+ account_t * top_account() {
+ foreach (state_t& state, state_stack)
+ if (state.type() == typeid(account_t *))
+ return boost::get<account_t *>(state);
+ return NULL;
+ }
+
+ void parse();
+ std::streamsize read_line(char *& line);
+ bool peek_whitespace_line() {
+ return (in.good() && ! in.eof() &&
+ (in.peek() == ' ' || in.peek() == '\t'));
+ }
+
+ void read_next_directive();
+
+#if defined(TIMELOG_SUPPORT)
+ void clock_in_directive(char * line, bool capitalized);
+ void clock_out_directive(char * line, bool capitalized);
+#endif
+
+ void default_commodity_directive(char * line);
+ void default_account_directive(char * line);
+ void price_conversion_directive(char * line);
+ void price_xact_directive(char * line);
+ void nomarket_directive(char * line);
+ void year_directive(char * line);
+ void option_directive(char * line);
+ void automated_xact_directive(char * line);
+ void period_xact_directive(char * line);
+ void xact_directive(char * line, std::streamsize len);
+ void include_directive(char * line);
+ void master_account_directive(char * line);
+ void end_directive(char * line);
+ void alias_directive(char * line);
+ void fixed_directive(char * line);
+ void tag_directive(char * line);
+ void define_directive(char * line);
+ bool general_directive(char * line);
+
+ post_t * parse_post(char * line,
+ std::streamsize len,
+ account_t * account,
+ xact_t * xact,
+ bool honor_strict = true,
+ bool defer_expr = false);
+
+ bool parse_posts(account_t * account,
+ xact_base_t& xact,
+ const bool defer_expr = false);
+
+ xact_t * parse_xact(char * line,
+ std::streamsize len,
+ account_t * account);
+
+ virtual expr_t::ptr_op_t lookup(const symbol_t::kind_t kind,
+ const string& name);
+ };
+
+ void parse_amount_expr(scope_t& scope,
+ std::istream& in,
+ amount_t& amount,
+ optional<expr_t> * amount_expr,
+ post_t * post,
+ const parse_flags_t& flags = PARSE_DEFAULT,
+ const bool defer_expr = false)
+ {
+ expr_t expr(in, flags.plus_flags(PARSE_PARTIAL));
+
+ DEBUG("textual.parse", "Parsed an amount expression");
+
+#if defined(DEBUG_ENABLED)
+ DEBUG_IF("textual.parse") {
+ if (_debug_stream) {
+ ledger::dump_value_expr(*_debug_stream, expr);
+ *_debug_stream << std::endl;
+ }
+ }
+#endif
+
+ if (expr) {
+ bind_scope_t bound_scope(scope, *post);
+ if (defer_expr) {
+ assert(amount_expr);
+ *amount_expr = expr;
+ (*amount_expr)->compile(bound_scope);
+ } else {
+ value_t result(expr.calc(bound_scope));
+ if (result.is_long()) {
+ amount = result.to_amount();
+ } else {
+ if (! result.is_amount())
+ throw_(amount_error,
+ _("Amount expressions must result in a simple amount"));
+ amount = result.as_amount();
+ }
+ DEBUG("textual.parse", "The posting amount is " << amount);
+ }
+ }
+ }
+}
+
+instance_t::instance_t(std::list<state_t>& _state_stack,
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& _timelog,
+#endif
+ std::istream& _in,
+ scope_t& _scope,
+ journal_t& _journal,
+ account_t * _master,
+ const path * _original_file,
+ bool _strict,
+ instance_t * _parent)
+ : state_stack(_state_stack),
+#if defined(TIMELOG_SUPPORT)
+ timelog(_timelog),
+#endif
+ parent(_parent), in(_in), scope(_scope),
+ journal(_journal), master(_master),
+ original_file(_original_file), strict(_strict)
+{
+ TRACE_CTOR(instance_t, "...");
+
+ if (! master)
+ master = journal.master;
+ state_stack.push_front(master);
+
+ if (_original_file)
+ pathname = *_original_file;
+ else
+ pathname = "/dev/stdin";
+}
+
+instance_t::~instance_t()
+{
+ TRACE_DTOR(instance_t);
+
+ assert(! state_stack.empty());
+ state_stack.pop_front();
+}
+
+void instance_t::parse()
+{
+ INFO("Parsing file '" << pathname.string() << "'");
+
+ TRACE_START(instance_parse, 1,
+ "Done parsing file '" << pathname.string() << "'");
+
+ if (! in.good() || in.eof())
+ return;
+
+ linenum = 0;
+ errors = 0;
+ count = 0;
+ curr_pos = in.tellg();
+
+ while (in.good() && ! in.eof()) {
+ try {
+ read_next_directive();
+ }
+ catch (const std::exception& err) {
+ string current_context = error_context();
+
+ if (parent) {
+ std::list<instance_t *> instances;
+
+ for (instance_t * instance = parent;
+ instance;
+ instance = instance->parent)
+ instances.push_front(instance);
+
+ foreach (instance_t * instance, instances)
+ add_error_context(_("In file included from %1")
+ << file_context(instance->pathname,
+ instance->linenum));
+ }
+ add_error_context(_("While parsing file %1")
+ << file_context(pathname, linenum));
+
+ if (caught_signal != NONE_CAUGHT)
+ throw;
+
+ string context = error_context();
+ if (! context.empty())
+ std::cerr << context << std::endl;
+
+ if (! current_context.empty())
+ std::cerr << current_context << std::endl;
+
+ std::cerr << _("Error: ") << err.what() << std::endl;
+ errors++;
+ }
+ }
+
+ TRACE_STOP(instance_parse, 1);
+}
+
+std::streamsize instance_t::read_line(char *& line)
+{
+ assert(in.good());
+ assert(! in.eof()); // no one should call us in that case
+
+ line_beg_pos = curr_pos;
+
+ check_for_signal();
+
+ in.getline(linebuf, MAX_LINE);
+ std::streamsize len = in.gcount();
+
+ if (len > 0) {
+ if (linenum == 0 && utf8::is_bom(linebuf))
+ line = &linebuf[3];
+ else
+ line = linebuf;
+
+ if (line[len - 1] == '\r') // strip Windows CRLF down to LF
+ line[--len] = '\0';
+
+ linenum++;
+
+ curr_pos = line_beg_pos;
+ curr_pos += len;
+
+ return len - 1; // LF is being silently dropped
+ }
+ return 0;
+}
+
+void instance_t::read_next_directive()
+{
+ char * line;
+ std::streamsize len = read_line(line);
+
+ if (len == 0 || line == NULL)
+ return;
+
+ switch (line[0]) {
+ case '\0':
+ assert(false); // shouldn't ever reach here
+ break;
+
+ case ' ':
+ case '\t': {
+ break;
+ }
+
+ case ';': // comments
+ case '#':
+ case '*':
+ case '|':
+ break;
+
+ case '-': // option setting
+ option_directive(line);
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ xact_directive(line, len);
+ break;
+ case '=': // automated xact
+ automated_xact_directive(line);
+ break;
+ case '~': // period xact
+ period_xact_directive(line);
+ break;
+
+ case '@':
+ case '!':
+ line++;
+ // fall through...
+ default: // some other directive
+ if (! general_directive(line)) {
+ switch (line[0]) {
+#if defined(TIMELOG_SUPPORT)
+ case 'i':
+ clock_in_directive(line, false);
+ break;
+ case 'I':
+ clock_in_directive(line, true);
+ break;
+
+ case 'o':
+ clock_out_directive(line, false);
+ break;
+ case 'O':
+ clock_out_directive(line, true);
+ break;
+
+ case 'h':
+ case 'b':
+ break;
+#endif // TIMELOG_SUPPORT
+
+ case 'A': // a default account for unbalanced posts
+ default_account_directive(line);
+ break;
+ case 'C': // a set of conversions
+ price_conversion_directive(line);
+ break;
+ case 'D': // a default commodity for "xact"
+ default_commodity_directive(line);
+ break;
+ case 'N': // don't download prices
+ nomarket_directive(line);
+ break;
+ case 'P': // a pricing xact
+ price_xact_directive(line);
+ break;
+ case 'Y': // set the current year
+ year_directive(line);
+ break;
+ }
+ }
+ break;
+ }
+}
+
+#if defined(TIMELOG_SUPPORT)
+
+void instance_t::clock_in_directive(char * line,
+ bool /*capitalized*/)
+{
+ string datetime(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ char * n = next_element(p, true);
+ char * end = n ? next_element(n, true) : NULL;
+
+ if (end && *end == ';')
+ end = skip_ws(end + 1);
+ else
+ end = NULL;
+
+ position_t position;
+ position.pathname = pathname;
+ position.beg_pos = line_beg_pos;
+ position.beg_line = linenum;
+ position.end_pos = curr_pos;
+ position.end_line = linenum;
+
+ time_xact_t event(position, parse_datetime(datetime, current_year),
+ p ? top_account()->find_account(p) : NULL,
+ n ? n : "",
+ end ? end : "");
+
+ timelog.clock_in(event);
+}
+
+void instance_t::clock_out_directive(char * line,
+ bool /*capitalized*/)
+{
+ string datetime(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ char * n = next_element(p, true);
+ char * end = n ? next_element(n, true) : NULL;
+
+ if (end && *end == ';')
+ end = skip_ws(end + 1);
+ else
+ end = NULL;
+
+ position_t position;
+ position.pathname = pathname;
+ position.beg_pos = line_beg_pos;
+ position.beg_line = linenum;
+ position.end_pos = curr_pos;
+ position.end_line = linenum;
+
+ time_xact_t event(position, parse_datetime(datetime, current_year),
+ p ? top_account()->find_account(p) : NULL,
+ n ? n : "",
+ end ? end : "");
+
+ timelog.clock_out(event);
+ count++;
+}
+
+#endif // TIMELOG_SUPPORT
+
+void instance_t::default_commodity_directive(char * line)
+{
+ amount_t amt(skip_ws(line + 1));
+ VERIFY(amt.valid());
+ commodity_pool_t::current_pool->default_commodity = &amt.commodity();
+ amt.commodity().add_flags(COMMODITY_KNOWN);
+}
+
+void instance_t::default_account_directive(char * line)
+{
+ journal.bucket = top_account()->find_account(skip_ws(line + 1));
+ journal.bucket->add_flags(ACCOUNT_KNOWN);
+}
+
+void instance_t::price_conversion_directive(char * line)
+{
+ if (char * p = std::strchr(line + 1, '=')) {
+ *p++ = '\0';
+ amount_t::parse_conversion(line + 1, p);
+ }
+}
+
+void instance_t::price_xact_directive(char * line)
+{
+ optional<std::pair<commodity_t *, price_point_t> > point =
+ commodity_pool_t::current_pool->parse_price_directive(skip_ws(line + 1));
+ if (! point)
+ throw parse_error(_("Pricing entry failed to parse"));
+}
+
+void instance_t::nomarket_directive(char * line)
+{
+ char * p = skip_ws(line + 1);
+ string symbol;
+ commodity_t::parse_symbol(p, symbol);
+
+ if (commodity_t * commodity =
+ commodity_pool_t::current_pool->find_or_create(symbol))
+ commodity->add_flags(COMMODITY_NOMARKET | COMMODITY_KNOWN);
+}
+
+void instance_t::year_directive(char * line)
+{
+ current_year = lexical_cast<unsigned short>(skip_ws(line + 1));
+}
+
+void instance_t::option_directive(char * line)
+{
+ char * p = next_element(line);
+ if (! p) {
+ p = std::strchr(line, '=');
+ if (p)
+ *p++ = '\0';
+ }
+
+ if (! process_option(pathname.string(), line + 2, scope, p, line))
+ throw_(option_error, _("Illegal option --%1") << line + 2);
+}
+
+void instance_t::automated_xact_directive(char * line)
+{
+ istream_pos_type pos = line_beg_pos;
+ std::size_t lnum = linenum;
+
+ bool reveal_context = true;
+
+ try {
+
+ std::auto_ptr<auto_xact_t> ae
+ (new auto_xact_t(query_t(string(skip_ws(line + 1)),
+ keep_details_t(true, true, true))));
+
+ reveal_context = false;
+
+ if (parse_posts(top_account(), *ae.get(), true)) {
+ reveal_context = true;
+
+ journal.auto_xacts.push_back(ae.get());
+
+ ae->journal = &journal;
+ ae->pos = position_t();
+ ae->pos->pathname = pathname;
+ ae->pos->beg_pos = pos;
+ ae->pos->beg_line = lnum;
+ ae->pos->end_pos = curr_pos;
+ ae->pos->end_line = linenum;
+
+ ae.release();
+ }
+
+ }
+ catch (const std::exception& err) {
+ if (reveal_context) {
+ add_error_context(_("While parsing periodic transaction:"));
+ add_error_context(source_context(pathname, pos, curr_pos, "> "));
+ }
+ throw;
+ }
+}
+
+void instance_t::period_xact_directive(char * line)
+{
+ istream_pos_type pos = line_beg_pos;
+ std::size_t lnum = linenum;
+
+ bool reveal_context = true;
+
+ try {
+
+ std::auto_ptr<period_xact_t> pe(new period_xact_t(skip_ws(line + 1)));
+
+ reveal_context = false;
+
+ if (parse_posts(top_account(), *pe.get())) {
+ reveal_context = true;
+ pe->journal = &journal;
+
+ if (pe->finalize()) {
+ journal.extend_xact(pe.get());
+ journal.period_xacts.push_back(pe.get());
+
+ pe->pos = position_t();
+ pe->pos->pathname = pathname;
+ pe->pos->beg_pos = pos;
+ pe->pos->beg_line = lnum;
+ pe->pos->end_pos = curr_pos;
+ pe->pos->end_line = linenum;
+
+ pe.release();
+ } else {
+ pe->journal = NULL;
+ throw parse_error(_("Period transaction failed to balance"));
+ }
+ }
+
+ }
+ catch (const std::exception& err) {
+ if (reveal_context) {
+ add_error_context(_("While parsing periodic transaction:"));
+ add_error_context(source_context(pathname, pos, curr_pos, "> "));
+ }
+ throw;
+ }
+}
+
+void instance_t::xact_directive(char * line, std::streamsize len)
+{
+ TRACE_START(xacts, 1, "Time spent handling transactions:");
+
+ if (xact_t * xact = parse_xact(line, len, top_account())) {
+ std::auto_ptr<xact_t> manager(xact);
+
+ if (journal.add_xact(xact)) {
+ manager.release(); // it's owned by the journal now
+ count++;
+ }
+ // It's perfectly valid for the journal to reject the xact, which it will
+ // do if the xact has no substantive effect (for example, a checking
+ // xact, all of whose postings have null amounts).
+ } else {
+ throw parse_error(_("Failed to parse transaction"));
+ }
+
+ TRACE_STOP(xacts, 1);
+}
+
+void instance_t::include_directive(char * line)
+{
+ path filename;
+
+ if (line[0] != '/' && line[0] != '\\' && line[0] != '~') {
+ string::size_type pos = pathname.string().rfind('/');
+ if (pos == string::npos)
+ pos = pathname.string().rfind('\\');
+ if (pos != string::npos)
+ filename = path(string(pathname.string(), 0, pos + 1)) / line;
+ } else {
+ filename = line;
+ }
+
+ filename = resolve_path(filename);
+
+ DEBUG("textual.include", "Line " << linenum << ": " <<
+ "Including path '" << filename << "'");
+
+ if (! exists(filename))
+ throw_(std::runtime_error,
+ _("File to include was not found: '%1'") << filename);
+
+ ifstream stream(filename);
+
+ instance_t instance(state_stack,
+#if defined(TIMELOG_SUPPORT)
+ timelog,
+#endif
+ stream, scope, journal, master,
+ &filename, strict, this);
+ instance.parse();
+
+ errors += instance.errors;
+ count += instance.count;
+}
+
+void instance_t::master_account_directive(char * line)
+{
+ if (account_t * acct = top_account()->find_account(line))
+ state_stack.push_front(acct);
+ else
+ assert(! "Failed to create account");
+}
+
+void instance_t::end_directive(char * kind)
+{
+ string name(kind);
+
+ if ((name.empty() || name == "account") && ! front_is_account())
+ throw_(std::runtime_error,
+ _("'end account' directive does not match open directive"));
+ else if (name == "tag" && ! front_is_string())
+ throw_(std::runtime_error,
+ _("'end tag' directive does not match open directive"));
+ else if (name == "fixed" && ! front_is_fixed_rate())
+ throw_(std::runtime_error,
+ _("'end fixed' directive does not match open directive"));
+
+ if (state_stack.size() <= 1)
+ throw_(std::runtime_error,
+ _("'end' found, but no enclosing tag or account directive"));
+ else
+ state_stack.pop_front();
+}
+
+void instance_t::alias_directive(char * line)
+{
+ char * b = skip_ws(line);
+ if (char * e = std::strchr(b, '=')) {
+ char * z = e - 1;
+ while (std::isspace(*z))
+ *z-- = '\0';
+ *e++ = '\0';
+ e = skip_ws(e);
+
+ // Once we have an alias name (b) and the target account
+ // name (e), add a reference to the account in the
+ // `account_aliases' map, which is used by the post
+ // parser to resolve alias references.
+ account_t * acct = top_account()->find_account(e);
+ std::pair<accounts_map::iterator, bool> result
+ = account_aliases.insert(accounts_map::value_type(b, acct));
+ assert(result.second);
+ }
+}
+
+void instance_t::fixed_directive(char * line)
+{
+ if (optional<std::pair<commodity_t *, price_point_t> > price_point =
+ commodity_pool_t::current_pool->parse_price_directive(trim_ws(line),
+ true)) {
+ state_stack.push_front(fixed_rate_t(price_point->first,
+ price_point->second.price));
+ } else {
+ throw_(std::runtime_error, _("Error in fixed directive"));
+ }
+}
+
+void instance_t::tag_directive(char * line)
+{
+ string tag(trim_ws(line));
+
+ if (tag.find(':') == string::npos)
+ tag = string(":") + tag + ":";
+
+ state_stack.push_front(tag);
+}
+
+void instance_t::define_directive(char * line)
+{
+ expr_t def(skip_ws(line));
+ def.compile(scope); // causes definitions to be established
+}
+
+bool instance_t::general_directive(char * line)
+{
+ char buf[8192];
+
+ std::strcpy(buf, line);
+
+ char * p = buf;
+ char * arg = next_element(buf);
+
+ if (*p == '@' || *p == '!')
+ p++;
+
+ switch (*p) {
+ case 'a':
+ if (std::strcmp(p, "account") == 0) {
+ master_account_directive(arg);
+ return true;
+ }
+ else if (std::strcmp(p, "alias") == 0) {
+ alias_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'b':
+ if (std::strcmp(p, "bucket") == 0) {
+ default_account_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'd':
+ if (std::strcmp(p, "def") == 0 || std::strcmp(p, "define") == 0) {
+ define_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'e':
+ if (std::strcmp(p, "end") == 0) {
+ end_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'f':
+ if (std::strcmp(p, "fixed") == 0) {
+ fixed_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'i':
+ if (std::strcmp(p, "include") == 0) {
+ include_directive(arg);
+ return true;
+ }
+ break;
+
+ case 't':
+ if (std::strcmp(p, "tag") == 0) {
+ tag_directive(arg);
+ return true;
+ }
+ break;
+
+ case 'y':
+ if (std::strcmp(p, "year") == 0) {
+ year_directive(arg);
+ return true;
+ }
+ break;
+ }
+
+ if (expr_t::ptr_op_t op = lookup(symbol_t::DIRECTIVE, p)) {
+ call_scope_t args(*this);
+ args.push_back(string_value(p));
+ op->as_function()(args);
+ return true;
+ }
+
+ return false;
+}
+
+post_t * instance_t::parse_post(char * line,
+ std::streamsize len,
+ account_t * account,
+ xact_t * xact,
+ bool honor_strict,
+ bool defer_expr)
+{
+ TRACE_START(post_details, 1, "Time spent parsing postings:");
+
+ std::auto_ptr<post_t> post(new post_t);
+
+ post->xact = xact; // this could be NULL
+ post->pos = position_t();
+ post->pos->pathname = pathname;
+ post->pos->beg_pos = line_beg_pos;
+ post->pos->beg_line = linenum;
+
+ char buf[MAX_LINE + 1];
+ std::strcpy(buf, line);
+ std::size_t beg = 0;
+
+ try {
+
+ // Parse the state flag
+
+ assert(line);
+ assert(*line);
+
+ char * p = skip_ws(line);
+
+ switch (*p) {
+ case '*':
+ post->set_state(item_t::CLEARED);
+ p = skip_ws(p + 1);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Parsed the CLEARED flag");
+ break;
+
+ case '!':
+ post->set_state(item_t::PENDING);
+ p = skip_ws(p + 1);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Parsed the PENDING flag");
+ break;
+ }
+
+ if (xact &&
+ ((xact->_state == item_t::CLEARED && post->_state != item_t::CLEARED) ||
+ (xact->_state == item_t::PENDING && post->_state == item_t::UNCLEARED)))
+ post->set_state(xact->_state);
+
+ // Parse the account name
+
+ if (! *p)
+ throw parse_error(_("Posting has no account"));
+
+ char * next = next_element(p, true);
+ char * e = p + std::strlen(p);
+
+ while (e > p && std::isspace(*(e - 1)))
+ e--;
+
+ if ((*p == '[' && *(e - 1) == ']') || (*p == '(' && *(e - 1) == ')')) {
+ post->add_flags(POST_VIRTUAL);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Parsed a virtual account name");
+
+ if (*p == '[') {
+ post->add_flags(POST_MUST_BALANCE);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Posting must balance");
+ }
+ p++; e--;
+ }
+
+ string name(p, e - p);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Parsed account name " << name);
+
+ if (account_aliases.size() > 0) {
+ accounts_map::const_iterator i = account_aliases.find(name);
+ if (i != account_aliases.end())
+ post->account = (*i).second;
+ }
+ if (! post->account)
+ post->account = account->find_account(name);
+
+ if (honor_strict && strict && ! post->account->has_flags(ACCOUNT_KNOWN)) {
+ if (post->_state == item_t::UNCLEARED)
+ warning_(_("\"%1\", line %2: Unknown account '%3'")
+ << pathname << linenum << post->account->fullname());
+ post->account->add_flags(ACCOUNT_KNOWN);
+ }
+
+ // Parse the optional amount
+
+ bool saw_amount = false;
+
+ if (next && *next && (*next != ';' && *next != '=')) {
+ saw_amount = true;
+
+ beg = next - line;
+ ptristream stream(next, len - beg);
+
+ if (*next != '(') // indicates a value expression
+ post->amount.parse(stream, PARSE_NO_REDUCE);
+ else
+ parse_amount_expr(scope, stream, post->amount, &post->amount_expr,
+ post.get(), PARSE_NO_REDUCE | PARSE_SINGLE |
+ PARSE_NO_ASSIGN, defer_expr);
+
+ if (! post->amount.is_null() && post->amount.has_commodity()) {
+ if (honor_strict && strict &&
+ ! post->amount.commodity().has_flags(COMMODITY_KNOWN)) {
+ if (post->_state == item_t::UNCLEARED)
+ warning_(_("\"%1\", line %2: Unknown commodity '%3'")
+ << pathname << linenum << post->amount.commodity());
+ post->amount.commodity().add_flags(COMMODITY_KNOWN);
+ }
+
+ if (! post->amount.has_annotation()) {
+ foreach (state_t& state, state_stack) {
+ if (state.type() == typeid(fixed_rate_t)) {
+ fixed_rate_t& rate(boost::get<fixed_rate_t>(state));
+ if (*rate.first == post->amount.commodity()) {
+ annotation_t details(rate.second);
+ details.add_flags(ANNOTATION_PRICE_FIXATED);
+ post->amount.annotate(details);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "post amount = " << post->amount);
+
+ if (stream.eof()) {
+ next = NULL;
+ } else {
+ next = skip_ws(next + static_cast<std::ptrdiff_t>(stream.tellg()));
+
+ // Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST)
+
+ if (*next == '@') {
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Found a price indicator");
+
+ bool per_unit = true;
+
+ if (*++next == '@') {
+ per_unit = false;
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "And it's for a total price");
+ }
+
+ beg = ++next - line;
+
+ p = skip_ws(next);
+ if (*p) {
+ post->cost = amount_t();
+
+ beg = p - line;
+ ptristream cstream(p, len - beg);
+
+ if (*p != '(') // indicates a value expression
+ post->cost->parse(cstream, PARSE_NO_MIGRATE);
+ else
+ parse_amount_expr(scope, cstream, *post->cost, NULL, post.get(),
+ PARSE_NO_MIGRATE | PARSE_SINGLE | PARSE_NO_ASSIGN,
+ defer_expr);
+
+ if (post->cost->sign() < 0)
+ throw parse_error(_("A posting's cost may not be negative"));
+
+ post->cost->in_place_unround();
+
+ if (per_unit) {
+ // For the sole case where the cost might be uncommoditized,
+ // guarantee that the commodity of the cost after multiplication
+ // is the same as it was before.
+ commodity_t& cost_commodity(post->cost->commodity());
+ *post->cost *= post->amount;
+ post->cost->set_commodity(cost_commodity);
+ }
+
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Total cost is " << *post->cost);
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Annotated amount is " << post->amount);
+
+ if (cstream.eof())
+ next = NULL;
+ else
+ next = skip_ws(p + static_cast<std::ptrdiff_t>(cstream.tellg()));
+ } else {
+ throw parse_error(_("Expected a cost amount"));
+ }
+ }
+ }
+ }
+
+ // Parse the optional balance assignment
+
+ if (xact && next && *next == '=') {
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Found a balance assignment indicator");
+
+ beg = ++next - line;
+
+ p = skip_ws(next);
+ if (*p) {
+ post->assigned_amount = amount_t();
+
+ beg = p - line;
+ ptristream stream(p, len - beg);
+
+ if (*p != '(') // indicates a value expression
+ post->assigned_amount->parse(stream, PARSE_NO_MIGRATE);
+ else
+ parse_amount_expr(scope, stream, *post->assigned_amount, NULL,
+ post.get(), PARSE_SINGLE | PARSE_NO_MIGRATE,
+ defer_expr);
+
+ if (post->assigned_amount->is_null()) {
+ if (post->amount.is_null())
+ throw parse_error(_("Balance assignment must evaluate to a constant"));
+ else
+ throw parse_error(_("Balance assertion must evaluate to a constant"));
+ }
+
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "POST assign: parsed amt = " << *post->assigned_amount);
+
+ amount_t& amt(*post->assigned_amount);
+ value_t account_total
+ (post->account->amount(false).strip_annotations(keep_details_t()));
+
+ DEBUG("post.assign",
+ "line " << linenum << ": " "account balance = " << account_total);
+ DEBUG("post.assign",
+ "line " << linenum << ": " "post amount = " << amt);
+
+ amount_t diff = amt;
+
+ switch (account_total.type()) {
+ case value_t::AMOUNT:
+ diff -= account_total.as_amount();
+ break;
+
+ case value_t::BALANCE:
+ if (optional<amount_t> comm_bal =
+ account_total.as_balance().commodity_amount(amt.commodity()))
+ diff -= *comm_bal;
+ break;
+
+ default:
+ break;
+ }
+
+ DEBUG("post.assign",
+ "line " << linenum << ": " << "diff = " << diff);
+ DEBUG("textual.parse",
+ "line " << linenum << ": " << "POST assign: diff = " << diff);
+
+ if (! diff.is_zero()) {
+ if (! post->amount.is_null()) {
+ diff -= post->amount;
+ if (! diff.is_zero())
+ throw_(parse_error, _("Balance assertion off by %1") << diff);
+ } else {
+ post->amount = diff;
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Overwrite null posting");
+ }
+ }
+
+ if (stream.eof())
+ next = NULL;
+ else
+ next = skip_ws(p + static_cast<std::ptrdiff_t>(stream.tellg()));
+ } else {
+ throw parse_error(_("Expected an balance assignment/assertion amount"));
+ }
+ }
+
+ // Parse the optional note
+
+ if (next && *next == ';') {
+ post->append_note(++next, current_year);
+ next = line + len;
+ DEBUG("textual.parse", "line " << linenum << ": "
+ << "Parsed a posting note");
+ }
+
+ // There should be nothing more to read
+
+ if (next && *next)
+ throw_(parse_error,
+ _("Unexpected char '%1' (Note: inline math requires parentheses)")
+ << *next);
+
+ post->pos->end_pos = curr_pos;
+ post->pos->end_line = linenum;
+
+ if (! state_stack.empty()) {
+ foreach (const state_t& state, state_stack)
+ if (state.type() == typeid(string))
+ post->parse_tags(boost::get<string>(state).c_str());
+ }
+
+ TRACE_STOP(post_details, 1);
+
+ return post.release();
+
+ }
+ catch (const std::exception& err) {
+ add_error_context(_("While parsing posting:"));
+ add_error_context(line_context(buf, beg, len));
+ throw;
+ }
+}
+
+bool instance_t::parse_posts(account_t * account,
+ xact_base_t& xact,
+ const bool defer_expr)
+{
+ TRACE_START(xact_posts, 1, "Time spent parsing postings:");
+
+ bool added = false;
+
+ while (peek_whitespace_line()) {
+ char * line;
+ std::streamsize len = read_line(line);
+ assert(len > 0);
+
+ if (post_t * post =
+ parse_post(line, len, account, NULL, /* honor_strict= */ false,
+ defer_expr)) {
+ xact.add_post(post);
+ added = true;
+ }
+ }
+
+ TRACE_STOP(xact_posts, 1);
+
+ return added;
+}
+
+xact_t * instance_t::parse_xact(char * line,
+ std::streamsize len,
+ account_t * account)
+{
+ TRACE_START(xact_text, 1, "Time spent parsing transaction text:");
+
+ std::auto_ptr<xact_t> xact(new xact_t);
+
+ xact->pos = position_t();
+ xact->pos->pathname = pathname;
+ xact->pos->beg_pos = line_beg_pos;
+ xact->pos->beg_line = linenum;
+
+ bool reveal_context = true;
+
+ try {
+
+ // Parse the date
+
+ char * next = next_element(line);
+
+ if (char * p = std::strchr(line, '=')) {
+ *p++ = '\0';
+ xact->_date_eff = parse_date(p, current_year);
+ }
+ xact->_date = parse_date(line, current_year);
+
+ // Parse the optional cleared flag: *
+
+ if (next) {
+ switch (*next) {
+ case '*':
+ xact->_state = item_t::CLEARED;
+ next = skip_ws(++next);
+ break;
+ case '!':
+ xact->_state = item_t::PENDING;
+ next = skip_ws(++next);
+ break;
+ }
+ }
+
+ // Parse the optional code: (TEXT)
+
+ if (next && *next == '(') {
+ if (char * p = std::strchr(next++, ')')) {
+ *p++ = '\0';
+ xact->code = next;
+ next = skip_ws(p);
+ }
+ }
+
+ // Parse the description text
+
+ if (next && *next) {
+ char * p = next_element(next, true);
+ xact->payee = next;
+ next = p;
+ } else {
+ xact->payee = _("<Unspecified payee>");
+ }
+
+ // Parse the xact note
+
+ if (next && *next == ';')
+ xact->append_note(++next, current_year);
+
+ TRACE_STOP(xact_text, 1);
+
+ // Parse all of the posts associated with this xact
+
+ TRACE_START(xact_details, 1, "Time spent parsing transaction details:");
+
+ post_t * last_post = NULL;
+
+ while (peek_whitespace_line()) {
+ len = read_line(line);
+
+ char * p = skip_ws(line);
+ if (! *p)
+ break;
+
+ if (*p == ';') {
+ item_t * item;
+ if (last_post)
+ item = last_post;
+ else
+ item = xact.get();
+
+ // This is a trailing note, and possibly a metadata info tag
+ item->append_note(p + 1, current_year);
+ item->pos->end_pos = curr_pos;
+ item->pos->end_line++;
+ } else {
+ reveal_context = false;
+
+ if (post_t * post =
+ parse_post(p, len - (p - line), account, xact.get())) {
+ xact->add_post(post);
+ last_post = post;
+ }
+ }
+ }
+
+ if (xact->_state == item_t::UNCLEARED) {
+ item_t::state_t result = item_t::CLEARED;
+
+ foreach (post_t * post, xact->posts) {
+ if (post->_state == item_t::UNCLEARED) {
+ result = item_t::UNCLEARED;
+ break;
+ }
+ else if (post->_state == item_t::PENDING) {
+ result = item_t::PENDING;
+ }
+ }
+ }
+
+ xact->pos->end_pos = curr_pos;
+ xact->pos->end_line = linenum;
+
+ if (! state_stack.empty()) {
+ foreach (const state_t& state, state_stack)
+ if (state.type() == typeid(string))
+ xact->parse_tags(boost::get<string>(state).c_str());
+ }
+
+ TRACE_STOP(xact_details, 1);
+
+ return xact.release();
+
+ }
+ catch (const std::exception& err) {
+ if (reveal_context) {
+ add_error_context(_("While parsing transaction:"));
+ add_error_context(source_context(xact->pos->pathname,
+ xact->pos->beg_pos, curr_pos, "> "));
+ }
+ throw;
+ }
+}
+
+expr_t::ptr_op_t instance_t::lookup(const symbol_t::kind_t kind,
+ const string& name)
+{
+ return scope.lookup(kind, name);
+}
+
+std::size_t journal_t::parse(std::istream& in,
+ scope_t& scope,
+ account_t * master,
+ const path * original_file,
+ bool strict)
+{
+ TRACE_START(parsing_total, 1, "Total time spent parsing text:");
+
+ std::list<instance_t::state_t> state_stack;
+#if defined(TIMELOG_SUPPORT)
+ time_log_t timelog(*this);
+#endif
+
+ instance_t parsing_instance(state_stack,
+#if defined(TIMELOG_SUPPORT)
+ timelog,
+#endif
+ in, scope, *this, master,
+ original_file, strict);
+ parsing_instance.parse();
+
+ TRACE_STOP(parsing_total, 1);
+
+ // These tracers were started in textual.cc
+ TRACE_FINISH(xact_text, 1);
+ TRACE_FINISH(xact_details, 1);
+ TRACE_FINISH(xact_posts, 1);
+ TRACE_FINISH(xacts, 1);
+ TRACE_FINISH(instance_parse, 1); // report per-instance timers
+ TRACE_FINISH(parsing_total, 1);
+
+ if (parsing_instance.errors > 0)
+ throw static_cast<int>(parsing_instance.errors);
+
+ return parsing_instance.count;
+}
+
+} // namespace ledger