summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2009-01-29 19:11:36 -0400
committerJohn Wiegley <johnw@newartisans.com>2009-01-29 19:11:36 -0400
commitc96635fe605f142f0402e5728e5d2a49b48c1453 (patch)
treea09d0bf7171a363e47c945f6e7f32b715ec10c4f
parent3c0e2138fc781b902e966459460943f8f838aa94 (diff)
downloadfork-ledger-c96635fe605f142f0402e5728e5d2a49b48c1453.tar.gz
fork-ledger-c96635fe605f142f0402e5728e5d2a49b48c1453.tar.bz2
fork-ledger-c96635fe605f142f0402e5728e5d2a49b48c1453.zip
Refactored the textual parser, to allow easy extensions in Python.
-rw-r--r--src/textual.cc1108
-rw-r--r--src/textual.h127
-rw-r--r--src/timelog.cc13
-rw-r--r--src/timelog.h2
4 files changed, 700 insertions, 550 deletions
diff --git a/src/textual.cc b/src/textual.cc
index 8d1a0029..e5906b3d 100644
--- a/src/textual.cc
+++ b/src/textual.cc
@@ -34,52 +34,67 @@
#endif
#include "textual.h"
+#if defined(TIMELOG_SUPPORT)
+#include "timelog.h"
+#endif
#include "expr.h"
#include "parser.h"
#include "session.h"
#include "option.h"
#include "acconf.h"
-#define TIMELOG_SUPPORT 1
-
namespace ledger {
-#define MAX_LINE 1024
+bool textual_parser_t::test(std::istream& in) const
+{
+ char buf[12];
+ char * p;
-static path pathname;
-static unsigned int linenum;
-static unsigned int src_idx;
-static accounts_map account_aliases;
+ in.read(buf, 11);
+ if (utf8::is_bom(buf))
+ p = &buf[3];
+ else
+ p = buf;
+
+ if (std::strncmp(p, "<?xml", 5) == 0)
+ throw_(parse_error,
+ "Ledger file contains XML data, but format was not recognized");
-static std::list<std::pair<path, int> > include_stack;
+ in.clear();
+ in.seekg(0, std::ios::beg);
+ assert(in.good());
+ return true;
+}
-#ifdef TIMELOG_SUPPORT
-struct time_entry_t
+std::size_t textual_parser_t::parse(std::istream& in,
+ session_t& session,
+ journal_t& journal,
+ account_t * master,
+ const path * original_file)
{
- datetime_t checkin;
- account_t * account;
- string desc;
+ TRACE_START(parsing_total, 1, "Total time spent parsing text:");
- time_entry_t() : account(NULL) {
- TRACE_CTOR(time_entry_t, "");
- }
- time_entry_t(const datetime_t& _checkin,
- account_t * _account = NULL,
- const string& _desc = "")
- : checkin(_checkin), account(_account), desc(_desc) {
- TRACE_CTOR(time_entry_t, "const datetime_t&, account_t *, const string&");
- }
- time_entry_t(const time_entry_t& entry)
- : checkin(entry.checkin), account(entry.account),
- desc(entry.desc) {
- TRACE_CTOR(time_entry_t, "copy");
- }
- ~time_entry_t() throw() {
- TRACE_DTOR(time_entry_t);
- }
-};
+ std::list<account_t *> account_stack;
+#if defined(TIMELOG_SUPPORT)
+ time_log_t timelog(journal);
#endif
+ instance_t parsing_instance(account_stack,
+#if defined(TIMELOG_SUPPORT)
+ timelog,
+#endif
+ in, session, journal, master,
+ original_file);
+ parsing_instance.parse();
+
+ TRACE_STOP(parsing_total, 1);
+
+ if (parsing_instance.errors > 0)
+ throw static_cast<int>(parsing_instance.errors);
+
+ return parsing_instance.count;
+}
+
namespace {
optional<expr_t> parse_amount_expr(std::istream& in,
amount_t& amount,
@@ -88,10 +103,9 @@ namespace {
{
expr_t expr(in, flags | static_cast<uint_least8_t>(expr_t::PARSE_PARTIAL));
- DEBUG("textual.parse", "line " << linenum << ": " <<
- "Parsed an amount expression");
+ DEBUG("textual.parse", "Parsed an amount expression");
-#ifdef DEBUG_ENABLED
+#if defined(DEBUG_ENABLED)
DEBUG_IF("textual.parse") {
if (_debug_stream) {
ledger::dump_value_expr(*_debug_stream, expr);
@@ -102,15 +116,539 @@ namespace {
if (expr) {
amount = expr.calc(*xact).as_amount();
- DEBUG("textual.parse", "line " << linenum << ": " <<
- "The transaction amount is " << amount);
+ DEBUG("textual.parse", "The transaction amount is " << amount);
return expr;
}
return none;
}
}
-xact_t * parse_xact(char * line, account_t * account, entry_t * entry = NULL)
+textual_parser_t::instance_t::instance_t
+ (std::list<account_t *>& _account_stack,
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& _timelog,
+#endif
+ std::istream& _in,
+ session_t& _session,
+ journal_t& _journal,
+ account_t * _master,
+ const path * _original_file,
+ instance_t * _parent)
+ : account_stack(_account_stack),
+#if defined(TIMELOG_SUPPORT)
+ timelog(_timelog),
+#endif
+ parent(_parent), in(_in), session(_session),
+ journal(_journal), master(_master),
+ original_file(_original_file)
+{
+ TRACE_CTOR(textual_parser_t::instance_t, "...");
+
+ if (! master)
+ master = journal.master;
+
+ account_stack.push_front(master);
+
+ pathname = journal.sources.back();
+ src_idx = journal.sources.size() - 1;
+ linenum = 1;
+ beg_pos = in.tellg();
+ beg_line = linenum;
+ count = 0;
+ errors = 0;
+}
+
+textual_parser_t::instance_t::~instance_t()
+{
+ TRACE_DTOR(textual_parser_t::instance_t);
+
+ account_stack.pop_front();
+
+ if (auto_entry_finalizer.get())
+ journal.remove_entry_finalizer(auto_entry_finalizer.get());
+}
+
+void textual_parser_t::instance_t::parse()
+{
+ INFO("Parsing file '" << pathname.string() << "'");
+
+ TRACE_START(instance_parse, 1,
+ "Done parsing file '" << pathname.string() << "'");
+
+ errors = 0;
+
+ while (in.good() && ! in.eof()) {
+ try {
+ read_next_directive();
+
+ beg_pos = end_pos;
+ }
+ catch (const std::exception& err) {
+ 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 '"
+ << file_context(instance->pathname,
+ instance->linenum - 1) << "':");
+ }
+ add_error_context("While parsing file '"
+ << file_context(pathname, linenum - 1) << "':");
+
+ std::cout.flush();
+ std::cerr << ledger::error_context()
+ << "Error: " << err.what() << std::endl;
+ errors++;
+ }
+ }
+
+ TRACE_STOP(instance_parse, 1);
+}
+
+void textual_parser_t::instance_t::read_next_directive()
+{
+ char * line;
+
+ in.getline(linebuf, MAX_LINE);
+ if (in.eof())
+ return;
+
+ if (linenum == 1 && utf8::is_bom(linebuf))
+ line = &linebuf[3];
+ else
+ line = linebuf;
+
+ int len = std::strlen(line);
+ if (line[len - 1] == '\r')
+ line[--len] = '\0';
+
+ end_pos = beg_pos;
+ end_pos += len + 1;
+ linenum++;
+
+ switch (line[0]) {
+ case '\0':
+ break;
+
+ case ' ':
+ case '\t': {
+ char * p = skip_ws(line);
+ if (*p)
+ throw parse_error("Line begins with whitespace");
+ break;
+ }
+
+ case '#': // comment line
+ case ';': // comment line
+ 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':
+ entry_directive(line);
+ break;
+ case '=': // automated entry
+ automated_entry_directive(line);
+ break;
+ case '~': // period entry
+ period_entry_directive(line);
+ break;
+
+#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 xacts
+ default_account_directive(line);
+ break;
+ case 'C': // a set of conversions
+ price_conversion_directive(line);
+ break;
+ case 'D': // a default commodity for "entry"
+ default_commodity_directive(line);
+ break;
+ case 'N': // don't download prices
+ nomarket_directive(line);
+ break;
+ case 'P': // a pricing entry
+ price_entry_directive(line);
+ break;
+ case 'Y': // set the current year
+ year_directive(line);
+ break;
+
+ case '@':
+ case '!':
+ line++;
+ // fall through...
+ default: // some other directive
+ general_directive(line);
+ break;
+ }
+}
+
+#if defined(TIMELOG_SUPPORT)
+
+void textual_parser_t::instance_t::clock_in_directive(char * line,
+ bool capitalized)
+{
+ string date(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ char * n = next_element(p, true);
+
+ timelog.clock_in(parse_datetime(date),
+ account_stack.front()->find_account(p), n ? n : "");
+}
+
+void textual_parser_t::instance_t::clock_out_directive(char * line,
+ bool capitalized)
+{
+ string date(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ char * n = next_element(p, true);
+
+ timelog.clock_out(parse_datetime(date),
+ p ? account_stack.front()->find_account(p) : NULL, n);
+ count++;
+}
+#endif // TIMELOG_SUPPORT
+
+void textual_parser_t::instance_t::default_commodity_directive(char * line)
+{
+ amount_t amt(skip_ws(line + 1));
+ assert(amt.valid());
+ amount_t::current_pool->default_commodity = &amt.commodity();
+}
+
+void textual_parser_t::instance_t::default_account_directive(char * line)
+{
+ journal.basket = account_stack.front()->find_account(skip_ws(line + 1));
+}
+
+void textual_parser_t::instance_t::price_conversion_directive(char * line)
+{
+ if (char * p = std::strchr(line + 1, '=')) {
+ *p++ = '\0';
+#if 0
+ // jww (2008-04-22): NYI!
+ parse_conversion(line + 1, p);
+#endif
+ }
+}
+
+namespace {
+ void parse_symbol(char *& p, string& symbol)
+ {
+ if (*p == '"') {
+ char * q = std::strchr(p + 1, '"');
+ if (! q)
+ throw parse_error("Quoted commodity symbol lacks closing quote");
+ symbol = string(p + 1, 0, q - p - 1);
+ p = q + 2;
+ } else {
+ char * q = next_element(p);
+ symbol = p;
+ if (q)
+ p = q;
+ else
+ p += symbol.length();
+ }
+ if (symbol.empty())
+ throw parse_error("Failed to parse commodity");
+ }
+}
+
+void textual_parser_t::instance_t::price_entry_directive(char * line)
+{
+ char * date_field_ptr = skip_ws(line + 1);
+ char * time_field_ptr = next_element(date_field_ptr);
+ if (! time_field_ptr) return;
+ string date_field = date_field_ptr;
+
+ char * symbol_and_price;
+ datetime_t datetime;
+
+ if (std::isdigit(time_field_ptr[0])) {
+ symbol_and_price = next_element(time_field_ptr);
+ if (! symbol_and_price) return;
+ datetime = parse_datetime(date_field + " " + time_field_ptr);
+ } else {
+ symbol_and_price = time_field_ptr;
+ datetime = parse_datetime(date_field);
+ }
+
+ string symbol;
+ parse_symbol(symbol_and_price, symbol);
+ amount_t price(symbol_and_price);
+ assert(price.valid());
+
+ if (commodity_t * commodity =
+ amount_t::current_pool->find_or_create(symbol))
+ commodity->add_price(datetime, price);
+}
+
+void textual_parser_t::instance_t::nomarket_directive(char * line)
+{
+ char * p = skip_ws(line + 1);
+ string symbol;
+ parse_symbol(p, symbol);
+
+ if (commodity_t * commodity =
+ amount_t::current_pool->find_or_create(symbol))
+ commodity->add_flags(COMMODITY_NOMARKET);
+}
+
+void textual_parser_t::instance_t::year_directive(char * line)
+{
+ current_year = std::atoi(skip_ws(line + 1));
+}
+
+void textual_parser_t::instance_t::option_directive(char * line)
+{
+ char * p = next_element(line);
+ if (! p) {
+ p = std::strchr(line, '=');
+ if (p)
+ *p++ = '\0';
+ }
+ process_option(line + 2, session, p);
+}
+
+void textual_parser_t::instance_t::automated_entry_directive(char * line)
+{
+ if (! auto_entry_finalizer.get()) {
+ auto_entry_finalizer.reset(new auto_entry_finalizer_t(&journal));
+ journal.add_entry_finalizer(auto_entry_finalizer.get());
+ }
+
+ auto_entry_t * ae = new auto_entry_t(skip_ws(line + 1));
+ if (parse_xacts(in, account_stack.front(), *ae, "automated",
+ end_pos)) {
+ journal.auto_entries.push_back(ae);
+ ae->src_idx = src_idx;
+ ae->beg_pos = beg_pos;
+ ae->beg_line = beg_line;
+ ae->end_pos = end_pos;
+ ae->end_line = linenum;
+ }
+}
+
+void textual_parser_t::instance_t::period_entry_directive(char * line)
+{
+ period_entry_t * pe = new period_entry_t(skip_ws(line + 1));
+ if (! pe->period)
+ throw_(parse_error, "Parsing time period '" << line << "'");
+
+ if (parse_xacts(in, account_stack.front(), *pe,
+ "period", end_pos)) {
+ if (pe->finalize()) {
+ extend_entry_base(&journal, *pe, true);
+ journal.period_entries.push_back(pe);
+ pe->src_idx = src_idx;
+ pe->beg_pos = beg_pos;
+ pe->beg_line = beg_line;
+ pe->end_pos = end_pos;
+ pe->end_line = linenum;
+ } else {
+ throw parse_error("Period entry failed to balance");
+ }
+ }
+}
+
+void textual_parser_t::instance_t::entry_directive(char * line)
+{
+ istream_pos_type pos = beg_pos;
+
+ TRACE_START(entries, 1, "Time spent handling entries:");
+
+ if (entry_t * entry = parse_entry(in, line, account_stack.front(), pos)) {
+ // The entry pointer is unowned at the minute, and there is a
+ // possibility that add_entry ma throw an exception, which
+ // would cause us to leak without this guard.
+ std::auto_ptr<entry_t> entry_ptr(entry);
+ if (journal.add_entry(entry)) {
+ entry_ptr.release(); // it's owned by the journal now
+ entry->src_idx = src_idx;
+ entry->beg_pos = beg_pos;
+ entry->beg_line = beg_line;
+ entry->end_pos = pos;
+ entry->end_line = linenum;
+ count++;
+ }
+ // It's perfectly valid for the journal to reject the entry,
+ // which it will do if the entry has no substantive effect
+ // (for example, a checking entry, all of whose transactions
+ // have null amounts).
+ } else {
+ throw parse_error("Failed to parse entry");
+ }
+
+ end_pos = pos;
+
+ TRACE_STOP(entries, 1);
+}
+
+void textual_parser_t::instance_t::include_directive(char * line)
+{
+ path filename(next_element(line));
+
+#if 0
+ if (filename[0] != '/' && filename[0] != '\\' && filename[0] != '~') {
+ string::size_type pos = pathname.prev.rfind('/');
+ if (pos == string::npos)
+ pos = pathname.prev.rfind('\\');
+ if (pos != string::npos)
+ filename = string(pathname.prev, 0, pos + 1) + filename;
+ }
+#endif
+ filename = resolve_path(filename);
+
+ DEBUG("textual.include", "Line " << linenum << ": " <<
+ "Including path '" << filename << "'");
+
+ ifstream stream(filename);
+
+ instance_t instance(account_stack,
+#if defined(TIMELOG_SUPPORT)
+ timelog,
+#endif
+ stream, session, journal, master,
+ &filename, this);
+ instance.parse();
+
+ errors += instance.errors;
+ count += instance.count;
+}
+
+void textual_parser_t::instance_t::account_directive(char * line)
+{
+ if (account_t * acct =
+ account_stack.front()->find_account(next_element(line)))
+ account_stack.push_front(acct);
+ else
+ assert(! "Failed to create account");
+}
+
+void textual_parser_t::instance_t::end_directive(char * line)
+{
+ account_stack.pop_front();
+}
+
+void textual_parser_t::instance_t::alias_directive(char * line)
+{
+ char * b = skip_ws(line + 1);
+ 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 xact
+ // parser to resolve alias references.
+ account_t * acct = account_stack.front()->find_account(e);
+ std::pair<accounts_map::iterator, bool> result
+ = account_aliases.insert(accounts_map::value_type(b, acct));
+ assert(result.second);
+ }
+}
+
+void textual_parser_t::instance_t::define_directive(char * line)
+{
+ expr_t def(skip_ws(line + 1));
+ def.compile(session); // causes definitions to be established
+}
+
+void textual_parser_t::instance_t::general_directive(char * line)
+{
+ char * p = next_element(line);
+ string word(line + 1);
+
+ switch (*p) {
+ case 'a':
+ if (std::strcmp(p, "account") == 0) {
+ account_directive(line);
+ return;
+ }
+ else if (std::strcmp(p, "alias") == 0) {
+ alias_directive(line);
+ return;
+ }
+ break;
+
+ case 'd':
+ if (std::strcmp(p, "def") == 0) {
+ define_directive(line);
+ return;
+ }
+ break;
+
+ case 'e':
+ if (std::strcmp(p, "end") == 0) {
+ end_directive(line);
+ return;
+ }
+ break;
+
+ case 'i':
+ if (std::strcmp(p, "include") == 0) {
+ include_directive(line);
+ return;
+ }
+ break;
+ }
+
+ static const std::size_t textdir_len = std::strlen("ledger_textdir_");
+ scoped_array<char> directive(new char[std::strlen(p) + textdir_len + 1]);
+ std::strcpy(directive.get(), "ledger_textdir_");
+ std::strcpy(directive.get() + textdir_len, p);
+
+ if (expr_t::ptr_op_t op = lookup(directive.get())) {
+ call_scope_t args(*this);
+ args.push_back(string_value(p));
+ op->as_function()(args);
+ }
+}
+
+xact_t * textual_parser_t::instance_t::parse_xact(char * line,
+ account_t * account,
+ entry_t * entry)
{
std::istringstream in(line);
@@ -434,11 +972,11 @@ xact_t * parse_xact(char * line, account_t * account, entry_t * entry = NULL)
}
}
-bool parse_xacts(std::istream& in,
- account_t * account,
- entry_base_t& entry,
- const string& kind,
- istream_pos_type beg_pos)
+bool textual_parser_t::instance_t::parse_xacts(std::istream& in,
+ account_t * account,
+ entry_base_t& entry,
+ const string& kind,
+ istream_pos_type beg_pos)
{
TRACE_START(entry_xacts, 1, "Time spent parsing transactions:");
@@ -462,7 +1000,7 @@ bool parse_xacts(std::istream& in,
if (! *p)
break;
}
- if (xact_t * xact = parse_xact(line, account)) {
+ if (xact_t * xact = parse_xact(line, account, NULL)) {
entry.add_xact(xact);
added = true;
}
@@ -473,8 +1011,10 @@ bool parse_xacts(std::istream& in,
return added;
}
-entry_t * parse_entry(std::istream& in, char * line, account_t * master,
- textual_parser_t& parser, istream_pos_type& pos)
+entry_t * textual_parser_t::instance_t::parse_entry(std::istream& in,
+ char * line,
+ account_t * master,
+ istream_pos_type& pos)
{
TRACE_START(entry_text, 1, "Time spent preparing entry text:");
@@ -575,481 +1115,9 @@ entry_t * parse_entry(std::istream& in, char * line, account_t * master,
return curr.release();
}
-static inline void parse_symbol(char *& p, string& symbol)
+expr_t::ptr_op_t textual_parser_t::instance_t::lookup(const string& name)
{
- if (*p == '"') {
- char * q = std::strchr(p + 1, '"');
- if (! q)
- throw parse_error("Quoted commodity symbol lacks closing quote");
- symbol = string(p + 1, 0, q - p - 1);
- p = q + 2;
- } else {
- char * q = next_element(p);
- symbol = p;
- if (q)
- p = q;
- else
- p += symbol.length();
- }
- if (symbol.empty())
- throw parse_error("Failed to parse commodity");
-}
-
-bool textual_parser_t::test(std::istream& in) const
-{
- char buf[12];
- char * p;
-
- in.read(buf, 11);
- if (utf8::is_bom(buf))
- p = &buf[3];
- else
- p = buf;
-
- if (std::strncmp(p, "<?xml", 5) == 0)
- throw_(parse_error,
- "Ledger file contains XML data, but format was not recognized");
-
- in.clear();
- in.seekg(0, std::ios::beg);
- assert(in.good());
- return true;
-}
-
-static void clock_out_from_timelog(std::list<time_entry_t>& time_entries,
- const datetime_t& when,
- account_t * account,
- const char * desc,
- journal_t& journal)
-{
- time_entry_t event;
-
- if (time_entries.size() == 1) {
- event = time_entries.back();
- time_entries.clear();
- }
- else if (time_entries.empty()) {
- throw parse_error("Timelog check-out event without a check-in");
- }
- else if (! account) {
- throw parse_error
- ("When multiple check-ins are active, checking out requires an account");
- }
- else {
- bool found = false;
-
- for (std::list<time_entry_t>::iterator i = time_entries.begin();
- i != time_entries.end();
- i++)
- if (account == (*i).account) {
- event = *i;
- found = true;
- time_entries.erase(i);
- break;
- }
-
- if (! found)
- throw parse_error
- ("Timelog check-out event does not match any current check-ins");
- }
-
- if (desc && event.desc.empty()) {
- event.desc = desc;
- desc = NULL;
- }
-
- std::auto_ptr<entry_t> curr(new entry_t);
- curr->_date = when.date();
- curr->code = desc ? desc : "";
- curr->payee = event.desc;
-
- if (when < event.checkin)
- throw parse_error
- ("Timelog check-out date less than corresponding check-in");
-
- char buf[32];
- std::sprintf(buf, "%lds", long((when - event.checkin).seconds()));
- amount_t amt;
- amt.parse(buf);
- assert(amt.valid());
-
- xact_t * xact = new xact_t(event.account, amt, XACT_VIRTUAL);
- xact->set_state(item_t::CLEARED);
- curr->add_xact(xact);
-
- if (! journal.add_entry(curr.get()))
- throw parse_error("Failed to record 'out' timelog entry");
- else
- curr.release();
-}
-
-unsigned int textual_parser_t::parse(std::istream& in,
- session_t& session,
- journal_t& journal,
- account_t * master,
- const path * original_file)
-{
- TRACE_START(parsing_total, 1, "Total time spent parsing text:");
-
- static bool added_auto_entry_hook = false;
- static char linebuf[MAX_LINE + 1];
- char * line;
- unsigned int count = 0;
- unsigned int errors = 0;
-
- std::list<account_t *> account_stack;
- auto_entry_finalizer_t auto_entry_finalizer(&journal);
- std::list<time_entry_t> time_entries;
-
- if (! master)
- master = journal.master;
-
- account_stack.push_front(master);
-
- pathname = journal.sources.back();
- src_idx = journal.sources.size() - 1;
- linenum = 1;
-
- INFO("Parsing file '" << pathname.string() << "'");
-
- istream_pos_type beg_pos = in.tellg();
- istream_pos_type end_pos;
- unsigned long beg_line = linenum;
-
- while (in.good() && ! in.eof()) {
- try {
- in.getline(linebuf, MAX_LINE);
- if (in.eof())
- break;
-
- if (linenum == 1 && utf8::is_bom(linebuf))
- line = &linebuf[3];
- else
- line = linebuf;
-
- int len = std::strlen(line);
- if (line[len - 1] == '\r')
- line[--len] = '\0';
-
- end_pos = beg_pos;
- end_pos += len + 1;
- linenum++;
-
- switch (line[0]) {
- case '\0':
- break;
-
- case ' ':
- case '\t': {
- char * p = skip_ws(line);
- if (*p)
- throw parse_error("Line begins with whitespace");
- break;
- }
-
-#ifdef TIMELOG_SUPPORT
- case 'i':
- case 'I': {
- string date(line, 2, 19);
-
- char * p = skip_ws(line + 22);
- char * n = next_element(p, true);
-
- time_entry_t event(parse_datetime(date),
- account_stack.front()->find_account(p), n ? n : "");
-
- if (! time_entries.empty())
- foreach (time_entry_t& time_entry, time_entries)
- if (event.account == time_entry.account)
- throw parse_error("Cannot double check-in to the same account");
-
- time_entries.push_back(event);
- break;
- }
-
- case 'o':
- case 'O':
- if (time_entries.empty()) {
- throw parse_error("Timelog check-out event without a check-in");
- } else {
- string date(line, 2, 19);
-
- char * p = skip_ws(line + 22);
- char * n = next_element(p, true);
-
- clock_out_from_timelog
- (time_entries, parse_datetime(date),
- p ? account_stack.front()->find_account(p) : NULL, n, journal);
- count++;
- }
- break;
-#endif // TIMELOG_SUPPORT
-
- case 'D': { // a default commodity for "entry"
- amount_t amt(skip_ws(line + 1));
- assert(amt.valid());
- amount_t::current_pool->default_commodity = &amt.commodity();
- break;
- }
-
- case 'A': // a default account for unbalanced xacts
- journal.basket =
- account_stack.front()->find_account(skip_ws(line + 1));
- break;
-
- case 'C': // a set of conversions
- if (char * p = std::strchr(line + 1, '=')) {
- *p++ = '\0';
- // jww (2008-04-22): NYI!
-#if 0
- parse_conversion(line + 1, p);
-#endif
- }
- break;
-
- case 'P': { // a pricing entry
- char * date_field_ptr = skip_ws(line + 1);
- char * time_field_ptr = next_element(date_field_ptr);
- if (! time_field_ptr) break;
- string date_field = date_field_ptr;
-
- char * symbol_and_price;
- datetime_t datetime;
-
- if (std::isdigit(time_field_ptr[0])) {
- symbol_and_price = next_element(time_field_ptr);
- if (! symbol_and_price) break;
- datetime = parse_datetime(date_field + " " + time_field_ptr);
- } else {
- symbol_and_price = time_field_ptr;
- datetime = parse_datetime(date_field);
- }
-
- string symbol;
- parse_symbol(symbol_and_price, symbol);
- amount_t price(symbol_and_price);
- assert(price.valid());
-
- if (commodity_t * commodity =
- amount_t::current_pool->find_or_create(symbol))
- commodity->add_price(datetime, price);
- break;
- }
-
- case 'N': { // don't download prices
- char * p = skip_ws(line + 1);
- string symbol;
- parse_symbol(p, symbol);
-
- if (commodity_t * commodity =
- amount_t::current_pool->find_or_create(symbol))
- commodity->add_flags(COMMODITY_NOMARKET);
- break;
- }
-
- case 'Y': // set the current year
- current_year = std::atoi(skip_ws(line + 1));
- break;
-
-#ifdef TIMELOG_SUPPORT
- case 'h':
- case 'b':
-#endif
- case '*': // comment line
- case ';': // comment line
- break;
-
- case '-': { // option setting
- char * p = next_element(line);
- if (! p) {
- p = std::strchr(line, '=');
- if (p)
- *p++ = '\0';
- }
- process_option(line + 2, session, p);
- break;
- }
-
- case '=': { // automated entry
- if (! added_auto_entry_hook) {
- journal.add_entry_finalizer(&auto_entry_finalizer);
- added_auto_entry_hook = true;
- }
-
- auto_entry_t * ae = new auto_entry_t(skip_ws(line + 1));
- if (parse_xacts(in, account_stack.front(), *ae, "automated",
- end_pos)) {
- journal.auto_entries.push_back(ae);
- ae->src_idx = src_idx;
- ae->beg_pos = beg_pos;
- ae->beg_line = beg_line;
- ae->end_pos = end_pos;
- ae->end_line = linenum;
- }
- break;
- }
-
- case '~': { // period entry
- period_entry_t * pe = new period_entry_t(skip_ws(line + 1));
- if (! pe->period)
- throw_(parse_error, "Parsing time period '" << line << "'");
-
- if (parse_xacts(in, account_stack.front(), *pe,
- "period", end_pos)) {
- if (pe->finalize()) {
- extend_entry_base(&journal, *pe, true);
- journal.period_entries.push_back(pe);
- pe->src_idx = src_idx;
- pe->beg_pos = beg_pos;
- pe->beg_line = beg_line;
- pe->end_pos = end_pos;
- pe->end_line = linenum;
- } else {
- throw parse_error("Period entry failed to balance");
- }
- }
- break;
- }
-
- case '@':
- case '!': { // directive
- char * p = next_element(line);
- string word(line + 1);
- if (word == "include") {
- push_variable<path> save_pathname(pathname);
- push_variable<unsigned int> save_src_idx(src_idx);
- push_variable<istream_pos_type> save_beg_pos(beg_pos);
- push_variable<istream_pos_type> save_end_pos(end_pos);
- push_variable<unsigned int> save_linenum(linenum);
-
- pathname = p;
-#if 0
- if (pathname[0] != '/' && pathname[0] != '\\' && pathname[0] != '~') {
- string::size_type pos = save_pathname.prev.rfind('/');
- if (pos == string::npos)
- pos = save_pathname.prev.rfind('\\');
- if (pos != string::npos)
- pathname = string(save_pathname.prev, 0, pos + 1) + pathname;
- }
- pathname = resolve_path(pathname);
-
- DEBUG("ledger.textual.include", "line " << linenum << ": " <<
- "Including path '" << pathname << "'");
-
- include_stack.push_back(std::pair<path, int>
- (journal.sources.back(), linenum - 1));
- count += parse_journal_file(pathname, config, journal,
- account_stack.front());
- include_stack.pop_back();
-#endif
- }
- else if (word == "account") {
- account_t * acct;
- acct = account_stack.front()->find_account(p);
- account_stack.push_front(acct);
- }
- else if (word == "end") {
- account_stack.pop_front();
- }
- else if (word == "alias") {
- char * b = p;
- 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 xact
- // parser to resolve alias references.
- account_t * acct = account_stack.front()->find_account(e);
- std::pair<accounts_map::iterator, bool> result
- = account_aliases.insert(accounts_map::value_type(b, acct));
- assert(result.second);
- }
- }
- else if (word == "def") {
- expr_t def(p);
- def.compile(session); // causes definitions to be established
- }
- break;
- }
-
- default: {
- istream_pos_type pos = beg_pos;
- TRACE_START(entries, 1, "Time spent handling entries:");
- if (entry_t * entry =
- parse_entry(in, line, account_stack.front(), *this, pos)) {
- // The entry pointer is unowned at the minute, and there is a
- // possibility that add_entry ma throw an exception, which
- // would cause us to leak without this guard.
- std::auto_ptr<entry_t> entry_ptr(entry);
- if (journal.add_entry(entry)) {
- entry_ptr.release(); // it's owned by the journal now
- entry->src_idx = src_idx;
- entry->beg_pos = beg_pos;
- entry->beg_line = beg_line;
- entry->end_pos = pos;
- entry->end_line = linenum;
- count++;
- }
- // It's perfectly valid for the journal to reject the entry,
- // which it will do if the entry has no substantive effect
- // (for example, a checking entry, all of whose transactions
- // have null amounts).
- } else {
- throw parse_error("Failed to parse entry");
- }
- end_pos = pos;
- TRACE_STOP(entries, 1);
- break;
- }
- }
- }
- catch (const std::exception& err) {
- for (std::list<std::pair<path, int> >::reverse_iterator i =
- include_stack.rbegin();
- i != include_stack.rend();
- i++) {
- add_error_context("In file included from ");
-#if 0
- add_error_context(include_context((*i).first, (*i).second));
-#endif
- }
- add_error_context(file_context(pathname, linenum - 1));
-
- std::cout.flush();
- std::cerr << ledger::error_context()
- << "Error: " << err.what() << std::endl;
- errors++;
- }
- beg_pos = end_pos;
- }
-
- if (! time_entries.empty()) {
- std::list<account_t *> accounts;
-
- foreach (time_entry_t& time_entry, time_entries)
- accounts.push_back(time_entry.account);
-
- foreach (account_t * account, accounts)
- clock_out_from_timelog(time_entries, current_time, account, NULL,
- journal);
-
- assert(time_entries.empty());
- }
-
- if (added_auto_entry_hook)
- journal.remove_entry_finalizer(&auto_entry_finalizer);
-
- if (errors > 0)
- throw static_cast<int>(errors);
-
- TRACE_STOP(parsing_total, 1);
-
- return count;
+ return session.lookup(name);
}
void write_textual_journal(journal_t& journal,
@@ -1061,6 +1129,8 @@ void write_textual_journal(journal_t& journal,
unsigned long index = 0;
path found;
+ // jww (2009-01-29): This function currently doesn't work
+
if (pathname.empty()) {
if (! journal.sources.empty())
found = *journal.sources.begin();
diff --git a/src/textual.h b/src/textual.h
index 6cb12a56..8aa3a684 100644
--- a/src/textual.h
+++ b/src/textual.h
@@ -37,20 +37,113 @@
namespace ledger {
+#define TIMELOG_SUPPORT 1
+
+#if defined(TIMELOG_SUPPORT)
+class time_log_t;
+#endif
+
class textual_parser_t : public journal_t::parser_t
{
public:
virtual bool test(std::istream& in) const;
- virtual unsigned int parse(std::istream& in,
- session_t& session,
- journal_t& journal,
- account_t * master = NULL,
- const path * original_file = NULL);
-};
+ virtual std::size_t parse(std::istream& in,
+ session_t& session,
+ journal_t& journal,
+ account_t * master = NULL,
+ const path * original_file = NULL);
+
+protected:
+ class instance_t : public noncopyable, public scope_t
+ {
+ static const std::size_t MAX_LINE = 1024;
+
+ public:
+ std::list<account_t *>& account_stack;
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& timelog;
+#endif
+
+ instance_t * parent;
+ std::istream& in;
+ session_t& session;
+ journal_t& journal;
+ account_t * master;
+ const path * original_file;
+ accounts_map account_aliases;
+
+ path pathname;
+ std::size_t linenum;
+ std::size_t src_idx;
+ istream_pos_type beg_pos;
+ istream_pos_type end_pos;
+ unsigned long beg_line;
+ std::size_t count;
+ std::size_t errors;
+
+ char linebuf[MAX_LINE + 1];
+
+ scoped_ptr<auto_entry_finalizer_t> auto_entry_finalizer;
+
+ instance_t(std::list<account_t *>& _account_stack,
+#if defined(TIMELOG_SUPPORT)
+ time_log_t& _timelog,
+#endif
+ std::istream& _in,
+ session_t& _session,
+ journal_t& _journal,
+ account_t * _master = NULL,
+ const path * _original_file = NULL,
+ instance_t * _parent = NULL);
+
+ ~instance_t();
-xact_t * parse_xact_text(char * line, account_t * account);
-xact_t * parse_xact(std::istream& in, account_t * account);
+ void parse();
+ 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_entry_directive(char * line);
+ void nomarket_directive(char * line);
+ void year_directive(char * line);
+ void option_directive(char * line);
+ void automated_entry_directive(char * line);
+ void period_entry_directive(char * line);
+ void entry_directive(char * line);
+ void include_directive(char * line);
+ void account_directive(char * line);
+ void end_directive(char * line);
+ void alias_directive(char * line);
+ void define_directive(char * line);
+ void general_directive(char * line);
+
+ xact_t * parse_xact(char * line,
+ account_t * account,
+ entry_t * entry);
+
+ bool parse_xacts(std::istream& in,
+ account_t * account,
+ entry_base_t& entry,
+ const string& kind,
+ istream_pos_type beg_pos);
+
+ entry_t * parse_entry(std::istream& in,
+ char * line,
+ account_t * master,
+ istream_pos_type& pos);
+
+ virtual expr_t::ptr_op_t lookup(const string& name);
+ };
+
+ friend class instance_t;
+};
void write_textual_journal(journal_t& journal,
const path& pathname,
@@ -58,24 +151,6 @@ void write_textual_journal(journal_t& journal,
const string& write_hdr_format,
std::ostream& out);
-#if 0
-class include_context : public file_context
-{
- public:
- include_context(const path& file, unsigned long line,
- const string& desc = "") throw()
- : file_context(file, line, desc) {}
- virtual ~include_context() throw() {}
-
- virtual void describe(std::ostream& out) const throw() {
- if (! desc.empty())
- out << desc << ": ";
- out << "\"" << file.string() << "\", line " << line << ":"
- << std::endl;
- }
-};
-#endif
-
} // namespace ledger
#endif // _TEXTUAL_H
diff --git a/src/timelog.cc b/src/timelog.cc
index fc5f0a3e..31c8e6da 100644
--- a/src/timelog.cc
+++ b/src/timelog.cc
@@ -29,6 +29,8 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
+#include "timelog.h"
+
namespace ledger {
namespace {
@@ -119,8 +121,8 @@ time_log_t::~time_log_t()
}
void time_log_t::clock_in(const datetime_t& checkin,
- account_t * account = NULL,
- const string& desc = "")
+ account_t * account,
+ const string& desc)
{
time_entry_t event(checkin, account, desc);
@@ -133,13 +135,14 @@ void time_log_t::clock_in(const datetime_t& checkin,
}
void time_log_t::clock_out(const datetime_t& checkin,
- account_t * account = NULL,
- const string& desc = "")
+ account_t * account,
+ const string& desc)
{
if (time_entries.empty())
throw std::logic_error("Timelog check-out event without a check-in");
- clock_out_from_timelog(time_entries, checkin, account, desc, journal);
+ clock_out_from_timelog(time_entries, checkin, account, desc.c_str(),
+ journal);
}
} // namespace ledger
diff --git a/src/timelog.h b/src/timelog.h
index 5718a488..12de02d6 100644
--- a/src/timelog.h
+++ b/src/timelog.h
@@ -32,6 +32,8 @@
#ifndef _TIMELOG_H
#define _TIMELOG_H
+#include "journal.h"
+
namespace ledger {
class time_entry_t