diff options
author | John Wiegley <johnw@newartisans.com> | 2007-05-14 11:08:50 +0000 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2008-04-13 03:38:49 -0400 |
commit | 766a16131aaa89a576ae63dcf12ab6ad467d6655 (patch) | |
tree | 2040e179524025c1d52d3f1d322425ffa2decd2e /src | |
parent | 9044bf168ab0d8d6df03034898b1e5a4cb1559e8 (diff) | |
download | fork-ledger-766a16131aaa89a576ae63dcf12ab6ad467d6655.tar.gz fork-ledger-766a16131aaa89a576ae63dcf12ab6ad467d6655.tar.bz2 fork-ledger-766a16131aaa89a576ae63dcf12ab6ad467d6655.zip |
Begin work on splitting parsing into parse and compile phases.
Diffstat (limited to 'src')
-rw-r--r-- | src/builder.h | 16 | ||||
-rw-r--r-- | src/commodity.cc | 20 | ||||
-rw-r--r-- | src/commodity.h | 1 | ||||
-rw-r--r-- | src/compile.cc | 981 | ||||
-rw-r--r-- | src/textual.cc | 819 |
5 files changed, 1176 insertions, 661 deletions
diff --git a/src/builder.h b/src/builder.h index b5577ac1..d6d30a6b 100644 --- a/src/builder.h +++ b/src/builder.h @@ -16,15 +16,17 @@ namespace ledger { */ class builder_t { - virtual void pushAttribute(const string& name, - const string& value) = 0; - virtual void pushAttribute(const nameid_t name_id, - const string& value) = 0; +public: + struct position_t + { + typedef uint_least32_t file_pos_t; + typedef uint_least32_t file_line_t; - virtual void beginNode(const string& name) = 0; - virtual void beginNode(const nameid_t name_id) = 0; + path pathname; + file_pos_t offset; + file_line_t linenum; - virtual void appendText(const string& text) = 0; + position_t() : offset(0), linenum(0) {} virtual node_t * endNode(const optional<string>& name = none) = 0; virtual node_t * endNode(const nameid_t name_id) = 0; diff --git a/src/commodity.cc b/src/commodity.cc index b4302d67..9eaa3ad8 100644 --- a/src/commodity.cc +++ b/src/commodity.cc @@ -186,6 +186,26 @@ void commodity_t::parse_symbol(std::istream& in, string& symbol) symbol = buf; } +void commodity_t::parse_symbol(const 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"); +} + bool commodity_t::valid() const { if (symbol().empty() && this != parent().null_commodity) { diff --git a/src/commodity.h b/src/commodity.h index dbd6233f..2407ffec 100644 --- a/src/commodity.h +++ b/src/commodity.h @@ -190,6 +190,7 @@ public: optional<amount_t> value(const optional<moment_t>& moment = none); static void parse_symbol(std::istream& in, string& symbol); + static void parse_symbol(const char *& p, string& symbol); static string parse_symbol(std::istream& in) { string temp; parse_symbol(in, temp); diff --git a/src/compile.cc b/src/compile.cc new file mode 100644 index 00000000..f7e8e0b2 --- /dev/null +++ b/src/compile.cc @@ -0,0 +1,981 @@ +/* + * Copyright (c) 2003-2007, 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 "textual.h" +#include "session.h" + +namespace ledger { + +#define MAX_LINE 1024 + +static path pathname; +static unsigned int linenum; +static unsigned int src_idx; +static accounts_map account_aliases; + +typedef std::list<std::pair<path, int> > include_stack_t; + +static include_stack_t include_stack; + +#define TIMELOG_SUPPORT 1 +#ifdef TIMELOG_SUPPORT + +struct time_entry_t { + moment_t checkin; + account_t * account; + string desc; +}; + +std::list<time_entry_t> time_entries; + +#endif // TIMELOG_SUPPORT + +inline char * next_element(char * buf, bool variable = false) +{ + for (char * p = buf; *p; p++) { + if (! (*p == ' ' || *p == '\t')) + continue; + + if (! variable) { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*p == '\t') { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*(p + 1) == ' ') { + *p = '\0'; + return skip_ws(p + 2); + } + } + return NULL; +} + +static inline void +parse_amount_expr(std::istream& in, journal_t *, + transaction_t& xact, amount_t& amount, + unsigned short flags = 0) +{ + xml::xpath_t xpath(in, flags | XPATH_PARSE_RELAXED | XPATH_PARSE_PARTIAL); + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed an amount expression"); + +#if 0 + IF_DEBUG("ledger.textual.parse") { + if (_debug_stream) { + xpath.dump(*_debug_stream); + *_debug_stream << std::endl; + } + } +#endif + + amount = xpath.calc(xact.data).as_amount(); + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "The transaction amount is " << amount); +} + +transaction_t * parse_transaction(char * line, + journal_t * journal, + account_t * account, + entry_t * entry = NULL) +{ + // The account will be determined later... + std::auto_ptr<transaction_t> xact(new transaction_t(NULL)); + + // First cut up the input line into its various parts. + + char * state = NULL; + char * account_path = NULL; + char * amount = NULL; + char * note = NULL; + + char * p = line; + + if (*p == '*' || *p == '!') + state = p++; + + account_path = skip_ws(p); + + amount = next_element(account_path, true); + if (amount) { + char * p = amount; + while (*p && *p != ';') + p++; + + if (*p == ';') { + *p++ = '\0'; + note = skip_ws(p); + } + + p = amount + (std::strlen(amount) - 1); + while (p > amount && std::isspace(*p)) + p--; + + if (std::isspace(*(p + 1))) + *++p = '\0'; + } + + string err_desc; +#if 0 + try { +#endif + + xact->entry = entry; // this might be NULL + + // Parse the state flag + + if (state) + switch (*state) { + case '*': + xact->state = transaction_t::CLEARED; + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed the CLEARED flag"); + break; + case '!': + xact->state = transaction_t::PENDING; + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed the PENDING flag"); + break; + } + + // Parse the account name + + char * b = &account_path[0]; + char * e = &account_path[std::strlen(account_path) - 1]; + if ((*b == '[' && *e == ']') || + (*b == '(' && *e == ')')) { + xact->add_flags(TRANSACTION_VIRTUAL); + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed a virtual account name"); + if (*b == '[') { + xact->add_flags(TRANSACTION_BALANCE); + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed a balanced virtual account name"); + } + *account_path++ = '\0'; + *e = '\0'; + } + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed account name " << account_path); + if (account_aliases.size() > 0) { + accounts_map::const_iterator i = account_aliases.find(account_path); + if (i != account_aliases.end()) + xact->account = (*i).second; + } + if (! xact->account) + xact->account = account->find_account(account_path); + + // Parse the optional amount + + if (amount && *amount) { + std::istringstream in(amount); + + PUSH_CONTEXT(); + + // jww (2006-09-15): Make sure it doesn't gobble up the upcoming @ symbol + + unsigned long beg = (long)in.tellg(); + + amount_t temp; + temp.parse(in, AMOUNT_PARSE_NO_REDUCE); + xact->amount = temp; + + char c; + if (! in.eof() && (c = peek_next_nonws(in)) != '@' && + c != ';' && ! in.eof()) { + in.seekg(beg, std::ios::beg); + + if (xact->entry) { + // Create a report item for this entry, so the transaction + // below may refer to it + + if (! xact->entry->data) + xact->entry->data = xml::wrap_node(journal->document, xact->entry, + journal->document->top); + + xact->data = xml::wrap_node(journal->document, xact.get(), + xact->entry->data); + } + + assert(xact->amount); + parse_amount_expr(in, journal, *xact, *xact->amount, + XPATH_PARSE_NO_REDUCE); + + if (xact->entry) { + checked_delete(xact->data); + xact->data = NULL; + } + + unsigned long end = (long)in.tellg(); + + xact->amount_expr = string(line, beg, end - beg); + } + + // jww (2007-04-30): This should be a string context, or perhaps a + // file context + POP_CONTEXT(context("While parsing transaction amount")); + + // Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST) + + if (in.good() && ! in.eof()) { + char c = peek_next_nonws(in); + if (c == '@') { + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Found a price indicator"); + bool per_unit = true; + in.get(c); + if (in.peek() == '@') { + in.get(c); + per_unit = false; + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "And it's for a total price"); + } + + if (in.good() && ! in.eof()) { + PUSH_CONTEXT(); + + unsigned long beg = (long)in.tellg(); + + amount_t temp; + temp.parse(in); + xact->cost = temp; + + unsigned long end = (long)in.tellg(); + + if (per_unit) + xact->cost_expr = (string("@") + + string(amount, beg, end - beg)); + else + xact->cost_expr = (string("@@") + + string(amount, beg, end - beg)); + + POP_CONTEXT(context("While parsing transaction cost")); + + if (xact->cost->sign() < 0) + throw_(parse_error, "A transaction's cost may not be negative"); + + assert(xact->amount); + + amount_t per_unit_cost(*xact->cost); + if (per_unit) + *xact->cost *= xact->amount->number(); + else + per_unit_cost /= xact->amount->number(); + + if (xact->amount->commodity() && + ! xact->amount->commodity().annotated) + xact->amount->annotate_commodity(annotation_t(per_unit_cost, + xact->entry->actual_date(), + xact->entry->code)); + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Total cost is " << *xact->cost); + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Per-unit cost is " << per_unit_cost); + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Annotated amount is " << *xact->amount); + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Bare amount is " << xact->amount->number()); + } + } + } + + if (xact->amount) { + xact->amount->in_place_reduce(); + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Reduced amount is " << *xact->amount); + } + } + + // Parse the optional note + + if (note) { + xact->note = note; + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed a note '" << *xact->note << "'"); + + if (char * b = std::strchr(xact->note->c_str(), '[')) + if (char * e = std::strchr(xact->note->c_str(), ']')) { + char buf[256]; + std::strncpy(buf, b + 1, e - b - 1); + buf[e - b - 1] = '\0'; + + DEBUG("ledger.textual.parse", "line " << linenum << ": " << + "Parsed a transaction date " << buf); + + if (char * p = std::strchr(buf, '=')) { + *p++ = '\0'; + xact->_date_eff = parse_datetime(p); + } + if (buf[0]) + xact->_date = parse_datetime(buf); + } + } + + return xact.release(); + +#if 0 + } + catch (error * err) { + err->context.push_back + (new line_context(line, -1, ! err_desc.empty() ? + err_desc : "While parsing transaction:")); + throw err; + } +#endif +} + +bool parse_transactions(std::istream& in, + journal_t * journal, + account_t * account, + entry_base_t& entry, + const string& /* kind */, + unsigned long beg_pos) +{ + static char line[MAX_LINE + 1]; + bool added = false; + + while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) { + in.getline(line, MAX_LINE); + if (in.eof()) + break; + + beg_pos += std::strlen(line) + 1; + linenum++; + + char * p = skip_ws(line); + if (! *p || *p == '\r' || *p == '\n') + break; + + if (transaction_t * xact = parse_transaction(p, journal, account)) { + entry.add_transaction(xact); + added = true; + } + } + + return added; +} + +entry_t * parse_entry(std::istream& in, char * line, journal_t * journal, + account_t * master, textual_parser_t& /* parser */, + unsigned long beg_pos) +{ + TRACE_START(entry_text, 1, "Time spent preparing entry text:"); + + std::auto_ptr<entry_t> curr(new entry_t); + + // First cut up the input line into its various parts. + + char * date = NULL; + char * date_eff = NULL; + char * statep = NULL; + char * code = NULL; + char * payee = NULL; + + date = line; + + char * p = line; + + while (*p && (std::isdigit(*p) || *p == '/' || *p == '.' || *p == '-')) + p++; + assert(*p); + + if (*p == '=') { + *p++ = '\0'; + date_eff = p; + + while (*p && (std::isdigit(*p) || *p == '/' || *p == '.' || *p == '-')) + p++; + assert(*p); + } else { + *p++ = '\0'; + } + + p = skip_ws(p); + + if (*p == '*' || *p == '!') { + statep = p; + p++; *p++ = '\0'; + + p = skip_ws(p); + } + + if (*p == '(') { + code = ++p; + while (*p && *p != ')') + p++; + assert(*p); + *p++ = '\0'; + + p = skip_ws(p); + } + + payee = p; + + p = payee + (std::strlen(payee) - 1); + while (p > payee && std::isspace(*p)) + p--; + + if (std::isspace(*(p + 1))) + *++p = '\0'; + + TRACE_STOP(entry_text, 1); + + // Parse the date + + TRACE_START(entry_date, 1, "Time spent parsing entry dates:"); + + curr->_date = parse_datetime(date); + + if (date_eff) + curr->_date_eff = parse_datetime(date_eff); + + TRACE_STOP(entry_date, 1); + + // Parse the optional cleared flag: * + + TRACE_START(entry_details, 1, "Time spent parsing entry details:"); + + transaction_t::state_t state = transaction_t::UNCLEARED; + if (statep) { + switch (*statep) { + case '*': + state = transaction_t::CLEARED; + break; + case '!': + state = transaction_t::PENDING; + break; + } + } + + // Parse the optional code: (TEXT) + + if (code) + curr->code = code; + + // Parse the payee/description text + + assert(payee); + curr->payee = *payee != '\0' ? payee : "<Unspecified payee>"; + + TRACE_STOP(entry_details, 1); + + // Parse all of the transactions associated with this entry + + TRACE_START(entry_xacts, 1, "Time spent parsing transactions:"); + + unsigned long end_pos; + unsigned long beg_line = linenum; + + while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) { + line[0] = '\0'; + in.getline(line, MAX_LINE); + if (in.eof() || line[0] == '\0') + break; + end_pos = beg_pos + std::strlen(line) + 1; + linenum++; + + char * p = skip_ws(line); + if (! *p || *p == '\r' || *p == '\n') + break; + + if (transaction_t * xact = parse_transaction(p, journal, master, + curr.get())) { + if (state != transaction_t::UNCLEARED && + xact->state == transaction_t::UNCLEARED) + xact->state = state; + + xact->beg_pos = beg_pos; + xact->beg_line = beg_line; + xact->end_pos = end_pos; + xact->end_line = linenum; + beg_pos = end_pos; + + curr->add_transaction(xact); + } + + if (in.eof()) + break; + } + + if (curr->data) { + checked_delete(curr->data); + curr->data = NULL; + } + + TRACE_STOP(entry_xacts, 1); + + return curr.release(); +} + +static inline 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"); +} + +bool textual_parser_t::test(std::istream& in) const +{ + char buf[5]; + + in.read(buf, 5); + if (std::strncmp(buf, "<?xml", 5) == 0) { +#if defined(HAVE_EXPAT) || defined(HAVE_XMLPARSE) + throw_(parse_error, "Ledger file contains XML data, but format was not recognized"); +#else + throw_(parse_error, "Ledger file contains XML data, but no XML support present"); +#endif + } + + in.clear(); + in.seekg(0, std::ios::beg); + assert(in.good()); + return true; +} + +static void clock_out_from_timelog(const moment_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; + curr->code = desc ? desc : ""; + curr->payee = event.desc; + + if (curr->_date < event.checkin) + throw_(parse_error, + "Timelog check-out date less than corresponding check-in"); + + char buf[32]; + std::sprintf(buf, "%lds", (long)(curr->_date - event.checkin).total_seconds()); + amount_t amt; + amt.parse(buf); + + transaction_t * xact + = new transaction_t(event.account, amt, TRANSACTION_VIRTUAL); + xact->state = transaction_t::CLEARED; + curr->add_transaction(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, + journal_t * journal, + account_t * master, + const optional<path>& original) +{ + static bool added_auto_entry_hook = false; + static char line[MAX_LINE + 1]; + unsigned int count = 0; + + TRACE_START(parsing_total, 1, "Total time spent parsing text:"); + + std::list<account_t *> account_stack; + + auto_entry_finalizer_t auto_entry_finalizer(journal); + + if (! master && journal) + master = journal->master; + + account_stack.push_front(master); + + pathname = (journal ? journal->sources.back() : + (assert(original), *original)); + src_idx = journal ? journal->sources.size() - 1 : 0; + linenum = 1; + + INFO("Parsing file '" << pathname.string() << "'"); + + unsigned long beg_pos = in.tellg(); + unsigned long end_pos; + unsigned long beg_line = linenum; + + while (in.good() && ! in.eof()) { + in.getline(line, MAX_LINE); + if (in.eof()) + break; + end_pos = beg_pos + std::strlen(line) + 1; + linenum++; + + PUSH_CONTEXT(); + + switch (line[0]) { + case '\0': + case '\r': + break; + + case ' ': + case '\t': { + char * p = skip_ws(line); + if (*p && *p != '\r') + 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; + event.desc = n ? n : ""; + event.checkin = parse_datetime(date); + event.account = account_stack.front()->find_account(p); + + if (! time_entries.empty()) + for (std::list<time_entry_t>::iterator i = time_entries.begin(); + i != time_entries.end(); + i++) + if (event.account == (*i).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 + (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)); + 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'; + amount_t::parse_conversion(line + 1, p); + } + 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; + moment_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); + + 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_STYLE_NOMARKET); + break; + } + + case 'Y': // set current year +#if 0 + // jww (2007-04-18): Need to set this up again + date_t::current_year = lexical_cast<int>(skip_ws(line + 1)); +#endif + break; + +#ifdef TIMELOG_SUPPORT + case 'h': + case 'b': +#endif + case ';': // comment + break; + + case '-': // option setting + throw_(parse_error, "Option settings are not allowed in journal files"); + + case '=': { // automated entry + if (! added_auto_entry_hook) { + journal->add_entry_finalizer(&auto_entry_finalizer); + added_auto_entry_hook = true; + } + + std::auto_ptr<auto_entry_t> ae(new auto_entry_t(skip_ws(line + 1))); + if (parse_transactions(in, journal, account_stack.front(), *ae, + "automated", end_pos)) { + ae->src_idx = src_idx; + ae->beg_pos = beg_pos; + ae->beg_line = beg_line; + ae->end_pos = end_pos; + ae->end_line = linenum; + journal->auto_entries.push_back(ae.release()); + } + break; + } + + case '~': { // period entry + std::auto_ptr<period_entry_t> pe(new period_entry_t(skip_ws(line + 1))); + if (! pe->period) + throw_(parse_error, string("Parsing time period '") + skip_ws(line + 1) + "'"); + + if (parse_transactions(in, journal, account_stack.front(), *pe, + "period", end_pos)) { + if (pe->finalize()) { + extend_entry_base(journal, *pe, true); + pe->src_idx = src_idx; + pe->beg_pos = beg_pos; + pe->beg_line = beg_line; + pe->end_pos = end_pos; + pe->end_line = linenum; + journal->period_entries.push_back(pe.release()); + } 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") { + scoped_variable<path> save_path(pathname); + scoped_variable<unsigned int> save_src_idx(src_idx); + scoped_variable<unsigned long> save_beg_pos(beg_pos); + scoped_variable<unsigned long> save_end_pos(end_pos); + scoped_variable<unsigned int> save_linenum(linenum); + + if (*p != '~' && *p != '/') + pathname = (pathname.branch_path() / path(p)).normalize(); + else + pathname = resolve_path(p); + + DEBUG("ledger.textual.include", "Line " << linenum << ": " << + "Including path '" << pathname.string() << "'"); + + scoped_execute<void> + pop_include_stack(boost::bind(&include_stack_t::pop_back, + boost::ref(include_stack))); + include_stack.push_back + (std::pair<path, int>(journal->sources.back(), linenum - 1)); + + count += journal->session->read_journal(pathname, journal, + account_stack.front()); + } + else if (word == "account") { + if (account_t * acct = account_stack.front()->find_account(p)) + account_stack.push_front(acct); + else + ; // jww (2007-04-30): throw an error here + } + 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 transaction + // parser to resolve alias references. + if (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 { + ; // jww (2007-04-30): throw an error here + } + } + } + else if (word == "def" || word == "eval") { + // jww (2006-09-13): Read the string after and evaluate it. + // But also keep a list of these value expressions, and a + // way to know where they fall in the transaction sequence. + // This will be necessary so that binary file reading can + // re-evaluate them at the appopriate time. + + // compile(&journal->defs); + } + break; + } + + default: { + TRACE_START(entries, 1, "Time spent handling entries:"); + + std::auto_ptr<entry_t> entry + (parse_entry(in, line, journal, account_stack.front(), + *this, end_pos)); + if (entry.get()) { + entry->src_idx = src_idx; + entry->beg_pos = beg_pos; + entry->beg_line = beg_line; + entry->end_pos = end_pos; + entry->end_line = linenum; + + if (journal->add_entry(entry.get())) { + entry.release(); + count++; + } else { + throw_(parse_error, "Entry does not balance"); + } + } else { + throw_(parse_error, "Failed to parse entry"); + } + + TRACE_STOP(entries, 1); + break; + } + } + + POP_CONTEXT(file_context(pathname, beg_line, linenum, + beg_pos, end_pos)); + + beg_pos = end_pos; + beg_line = linenum; + } + + if (! time_entries.empty()) { + for (std::list<time_entry_t>::iterator i = time_entries.begin(); + i != time_entries.end(); + i++) + clock_out_from_timelog(now, (*i).account, NULL, journal); + time_entries.clear(); + } + + if (added_auto_entry_hook) + journal->remove_entry_finalizer(&auto_entry_finalizer); + + TRACE_STOP(parsing_total, 1); + + return count; +} + +} // namespace ledger diff --git a/src/textual.cc b/src/textual.cc index f7e8e0b2..00a4ba6c 100644 --- a/src/textual.cc +++ b/src/textual.cc @@ -30,89 +30,40 @@ */ #include "textual.h" -#include "session.h" namespace ledger { #define MAX_LINE 1024 -static path pathname; -static unsigned int linenum; -static unsigned int src_idx; -static accounts_map account_aliases; +typedef xml::builder_t::position_t position_t; -typedef std::list<std::pair<path, int> > include_stack_t; +namespace { + inline char * next_element(char * buf, bool variable = false) { + for (char * p = buf; *p; p++) { + if (! (*p == ' ' || *p == '\t')) + continue; -static include_stack_t include_stack; - -#define TIMELOG_SUPPORT 1 -#ifdef TIMELOG_SUPPORT - -struct time_entry_t { - moment_t checkin; - account_t * account; - string desc; -}; - -std::list<time_entry_t> time_entries; - -#endif // TIMELOG_SUPPORT - -inline char * next_element(char * buf, bool variable = false) -{ - for (char * p = buf; *p; p++) { - if (! (*p == ' ' || *p == '\t')) - continue; - - if (! variable) { - *p = '\0'; - return skip_ws(p + 1); - } - else if (*p == '\t') { - *p = '\0'; - return skip_ws(p + 1); - } - else if (*(p + 1) == ' ') { - *p = '\0'; - return skip_ws(p + 2); - } - } - return NULL; -} - -static inline void -parse_amount_expr(std::istream& in, journal_t *, - transaction_t& xact, amount_t& amount, - unsigned short flags = 0) -{ - xml::xpath_t xpath(in, flags | XPATH_PARSE_RELAXED | XPATH_PARSE_PARTIAL); - - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed an amount expression"); - -#if 0 - IF_DEBUG("ledger.textual.parse") { - if (_debug_stream) { - xpath.dump(*_debug_stream); - *_debug_stream << std::endl; + if (! variable) { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*p == '\t') { + *p = '\0'; + return skip_ws(p + 1); + } + else if (*(p + 1) == ' ') { + *p = '\0'; + return skip_ws(p + 2); + } } + return NULL; } -#endif - - amount = xpath.calc(xact.data).as_amount(); - - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "The transaction amount is " << amount); } -transaction_t * parse_transaction(char * line, - journal_t * journal, - account_t * account, - entry_t * entry = NULL) +void parse_transaction(builder_t& builder, + char * line, + position_t& end_of_line, { - // The account will be determined later... - std::auto_ptr<transaction_t> xact(new transaction_t(NULL)); - // First cut up the input line into its various parts. char * state = NULL; @@ -146,28 +97,20 @@ transaction_t * parse_transaction(char * line, *++p = '\0'; } - string err_desc; -#if 0 - try { -#endif + // Setup the details for this node - xact->entry = entry; // this might be NULL - - // Parse the state flag - - if (state) - switch (*state) { + if (statep) { + switch (*statep) { case '*': - xact->state = transaction_t::CLEARED; - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed the CLEARED flag"); + builder.push_attr(CLEARED_ATTR, "yes"); break; case '!': - xact->state = transaction_t::PENDING; - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed the PENDING flag"); + builder.push_attr(PENDING_ATTR, "yes"); break; } + } + + builder.begin_node(TRANSACTION_NODE); // Parse the account name @@ -175,229 +118,71 @@ transaction_t * parse_transaction(char * line, char * e = &account_path[std::strlen(account_path) - 1]; if ((*b == '[' && *e == ']') || (*b == '(' && *e == ')')) { - xact->add_flags(TRANSACTION_VIRTUAL); - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed a virtual account name"); - if (*b == '[') { - xact->add_flags(TRANSACTION_BALANCE); - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed a balanced virtual account name"); - } + builder.push_attr(VIRTUAL_ATTR, "yes"); + if (*b == '[') + builder.push_attr(BALANCE_ATTR, "yes"); *account_path++ = '\0'; *e = '\0'; } - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed account name " << account_path); - if (account_aliases.size() > 0) { - accounts_map::const_iterator i = account_aliases.find(account_path); - if (i != account_aliases.end()) - xact->account = (*i).second; - } - if (! xact->account) - xact->account = account->find_account(account_path); + builder.begin_node(ACCOUNT_PATH_NODE); + builder.append_text(account_path); + builder.end_node(ACCOUNT_PATH_NODE); // Parse the optional amount - if (amount && *amount) { - std::istringstream in(amount); - - PUSH_CONTEXT(); - - // jww (2006-09-15): Make sure it doesn't gobble up the upcoming @ symbol - - unsigned long beg = (long)in.tellg(); - - amount_t temp; - temp.parse(in, AMOUNT_PARSE_NO_REDUCE); - xact->amount = temp; - - char c; - if (! in.eof() && (c = peek_next_nonws(in)) != '@' && - c != ';' && ! in.eof()) { - in.seekg(beg, std::ios::beg); - - if (xact->entry) { - // Create a report item for this entry, so the transaction - // below may refer to it - - if (! xact->entry->data) - xact->entry->data = xml::wrap_node(journal->document, xact->entry, - journal->document->top); - - xact->data = xml::wrap_node(journal->document, xact.get(), - xact->entry->data); - } - - assert(xact->amount); - parse_amount_expr(in, journal, *xact, *xact->amount, - XPATH_PARSE_NO_REDUCE); - - if (xact->entry) { - checked_delete(xact->data); - xact->data = NULL; - } - - unsigned long end = (long)in.tellg(); - - xact->amount_expr = string(line, beg, end - beg); - } - - // jww (2007-04-30): This should be a string context, or perhaps a - // file context - POP_CONTEXT(context("While parsing transaction amount")); - - // Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST) - - if (in.good() && ! in.eof()) { - char c = peek_next_nonws(in); - if (c == '@') { - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Found a price indicator"); - bool per_unit = true; - in.get(c); - if (in.peek() == '@') { - in.get(c); - per_unit = false; - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "And it's for a total price"); - } - - if (in.good() && ! in.eof()) { - PUSH_CONTEXT(); - - unsigned long beg = (long)in.tellg(); - - amount_t temp; - temp.parse(in); - xact->cost = temp; - - unsigned long end = (long)in.tellg(); - - if (per_unit) - xact->cost_expr = (string("@") + - string(amount, beg, end - beg)); - else - xact->cost_expr = (string("@@") + - string(amount, beg, end - beg)); - - POP_CONTEXT(context("While parsing transaction cost")); - - if (xact->cost->sign() < 0) - throw_(parse_error, "A transaction's cost may not be negative"); - - assert(xact->amount); - - amount_t per_unit_cost(*xact->cost); - if (per_unit) - *xact->cost *= xact->amount->number(); - else - per_unit_cost /= xact->amount->number(); - - if (xact->amount->commodity() && - ! xact->amount->commodity().annotated) - xact->amount->annotate_commodity(annotation_t(per_unit_cost, - xact->entry->actual_date(), - xact->entry->code)); - - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Total cost is " << *xact->cost); - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Per-unit cost is " << per_unit_cost); - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Annotated amount is " << *xact->amount); - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Bare amount is " << xact->amount->number()); - } - } - } - - if (xact->amount) { - xact->amount->in_place_reduce(); - - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Reduced amount is " << *xact->amount); - } - } + builder.begin_node(AMOUNT_EXPR_NODE); + builder.append_text(account_path); + builder.end_node(AMOUNT_EXPR_NODE); // Parse the optional note if (note) { - xact->note = note; - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed a note '" << *xact->note << "'"); - - if (char * b = std::strchr(xact->note->c_str(), '[')) - if (char * e = std::strchr(xact->note->c_str(), ']')) { - char buf[256]; - std::strncpy(buf, b + 1, e - b - 1); - buf[e - b - 1] = '\0'; - - DEBUG("ledger.textual.parse", "line " << linenum << ": " << - "Parsed a transaction date " << buf); - - if (char * p = std::strchr(buf, '=')) { - *p++ = '\0'; - xact->_date_eff = parse_datetime(p); - } - if (buf[0]) - xact->_date = parse_datetime(buf); - } + builder.begin_node(NOTE_NODE); + builder.append_text(note); + builder.end_node(NOTE_NODE); } - return xact.release(); - -#if 0 - } - catch (error * err) { - err->context.push_back - (new line_context(line, -1, ! err_desc.empty() ? - err_desc : "While parsing transaction:")); - throw err; - } -#endif + builder.end_node(TRANSACTION_NODE, end_of_line); } -bool parse_transactions(std::istream& in, - journal_t * journal, - account_t * account, - entry_base_t& entry, - const string& /* kind */, - unsigned long beg_pos) +bool parse_transactions(std::istream& in, builder_t& builder) { - static char line[MAX_LINE + 1]; - bool added = false; + TRACE_START(entry_xacts, 1, "Time spent parsing transactions:"); + + bool added = false; while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) { + static char line[MAX_LINE + 1]; + line[0] = '\0'; in.getline(line, MAX_LINE); - if (in.eof()) + if (in.eof() || line[0] == '\0') break; - beg_pos += std::strlen(line) + 1; - linenum++; + position_t end_of_line(builder.position()); + end_of_line.offset += std::strlen(line) + 1; + end_of_line.linenum++; char * p = skip_ws(line); if (! *p || *p == '\r' || *p == '\n') break; - if (transaction_t * xact = parse_transaction(p, journal, account)) { - entry.add_transaction(xact); - added = true; - } + parse_transaction(builder, line, end_of_line); } + TRACE_STOP(entry_xacts, 1); + return added; } -entry_t * parse_entry(std::istream& in, char * line, journal_t * journal, - account_t * master, textual_parser_t& /* parser */, - unsigned long beg_pos) +void parse_entry(std::istream& in, + builder_t& builder, + char * line, + position_t& end_of_line) { TRACE_START(entry_text, 1, "Time spent preparing entry text:"); - std::auto_ptr<entry_t> curr(new entry_t); - - // First cut up the input line into its various parts. + // First cut up the input line into its various parts char * date = NULL; char * date_eff = NULL; @@ -454,111 +239,44 @@ entry_t * parse_entry(std::istream& in, char * line, journal_t * journal, TRACE_STOP(entry_text, 1); - // Parse the date + // Setup the details for this node - TRACE_START(entry_date, 1, "Time spent parsing entry dates:"); + TRACE_START(entry_details, 1, "Time spent parsing entry details:"); - curr->_date = parse_datetime(date); + builder.push_attr(DATE_ATTR, date); if (date_eff) - curr->_date_eff = parse_datetime(date_eff); - - TRACE_STOP(entry_date, 1); - - // Parse the optional cleared flag: * + builder.push_attr(DATE_EFF_ATTR, date_eff); - TRACE_START(entry_details, 1, "Time spent parsing entry details:"); - - transaction_t::state_t state = transaction_t::UNCLEARED; if (statep) { switch (*statep) { case '*': - state = transaction_t::CLEARED; + builder.push_attr(CLEARED_ATTR, "yes"); break; case '!': - state = transaction_t::PENDING; + builder.push_attr(PENDING_ATTR, "yes"); break; } } - // Parse the optional code: (TEXT) - if (code) - curr->code = code; + builder.push_attr(CODE_ATTR, code); - // Parse the payee/description text + builder.begin_node(ENTRY_NODE); + builder.begin_node(PAYEE_NODE); assert(payee); - curr->payee = *payee != '\0' ? payee : "<Unspecified payee>"; + builder.append_text(*payee != '\0' ? payee : "<Unspecified payee>"); + builder.end_node(PAYEE_NODE, end_of_line); TRACE_STOP(entry_details, 1); - // Parse all of the transactions associated with this entry - - TRACE_START(entry_xacts, 1, "Time spent parsing transactions:"); - - unsigned long end_pos; - unsigned long beg_line = linenum; - - while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) { - line[0] = '\0'; - in.getline(line, MAX_LINE); - if (in.eof() || line[0] == '\0') - break; - end_pos = beg_pos + std::strlen(line) + 1; - linenum++; - - char * p = skip_ws(line); - if (! *p || *p == '\r' || *p == '\n') - break; - - if (transaction_t * xact = parse_transaction(p, journal, master, - curr.get())) { - if (state != transaction_t::UNCLEARED && - xact->state == transaction_t::UNCLEARED) - xact->state = state; - - xact->beg_pos = beg_pos; - xact->beg_line = beg_line; - xact->end_pos = end_pos; - xact->end_line = linenum; - beg_pos = end_pos; - - curr->add_transaction(xact); - } - - if (in.eof()) - break; - } - - if (curr->data) { - checked_delete(curr->data); - curr->data = NULL; - } - - TRACE_STOP(entry_xacts, 1); + // Parse all the transactions associated with this entry - return curr.release(); -} + if (! parse_transactions(in, builder)) + throw_(parse_error, "Entry has no transactions"); -static inline 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"); + builder.end_node(ENTRY_NODE); } bool textual_parser_t::test(std::istream& in) const @@ -566,13 +284,8 @@ bool textual_parser_t::test(std::istream& in) const char buf[5]; in.read(buf, 5); - if (std::strncmp(buf, "<?xml", 5) == 0) { -#if defined(HAVE_EXPAT) || defined(HAVE_XMLPARSE) + if (std::strncmp(buf, "<?xml", 5) == 0) throw_(parse_error, "Ledger file contains XML data, but format was not recognized"); -#else - throw_(parse_error, "Ledger file contains XML data, but no XML support present"); -#endif - } in.clear(); in.seekg(0, std::ios::beg); @@ -580,109 +293,24 @@ bool textual_parser_t::test(std::istream& in) const return true; } -static void clock_out_from_timelog(const moment_t& when, - account_t * account, - const char * desc, - journal_t * journal) +void textual_parser_t::parse(std::istream& in, + builder_t& builder) { - 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; - curr->code = desc ? desc : ""; - curr->payee = event.desc; - - if (curr->_date < event.checkin) - throw_(parse_error, - "Timelog check-out date less than corresponding check-in"); - - char buf[32]; - std::sprintf(buf, "%lds", (long)(curr->_date - event.checkin).total_seconds()); - amount_t amt; - amt.parse(buf); - - transaction_t * xact - = new transaction_t(event.account, amt, TRANSACTION_VIRTUAL); - xact->state = transaction_t::CLEARED; - curr->add_transaction(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, - journal_t * journal, - account_t * master, - const optional<path>& original) -{ - static bool added_auto_entry_hook = false; - static char line[MAX_LINE + 1]; - unsigned int count = 0; - TRACE_START(parsing_total, 1, "Total time spent parsing text:"); - std::list<account_t *> account_stack; - - auto_entry_finalizer_t auto_entry_finalizer(journal); + INFO("Parsing file '" << builder.position().pathname.string() << "'"); - if (! master && journal) - master = journal->master; - - account_stack.push_front(master); - - pathname = (journal ? journal->sources.back() : - (assert(original), *original)); - src_idx = journal ? journal->sources.size() - 1 : 0; - linenum = 1; - - INFO("Parsing file '" << pathname.string() << "'"); - - unsigned long beg_pos = in.tellg(); - unsigned long end_pos; - unsigned long beg_line = linenum; + builder.begin_node(JOURNAL_NODE); while (in.good() && ! in.eof()) { + static char line[MAX_LINE + 1]; in.getline(line, MAX_LINE); if (in.eof()) break; - end_pos = beg_pos + std::strlen(line) + 1; - linenum++; + + position_t end_of_line(builder.position()); + end_of_line.offset += std::strlen(line) + 1; + end_of_line.linenum++; PUSH_CONTEXT(); @@ -699,7 +327,6 @@ unsigned int textual_parser_t::parse(std::istream& in, break; } -#ifdef TIMELOG_SUPPORT case 'i': case 'I': { string date(line, 2, 19); @@ -707,62 +334,56 @@ unsigned int textual_parser_t::parse(std::istream& in, char * p = skip_ws(line + 22); char * n = next_element(p, true); - time_entry_t event; - event.desc = n ? n : ""; - event.checkin = parse_datetime(date); - event.account = account_stack.front()->find_account(p); - - if (! time_entries.empty()) - for (std::list<time_entry_t>::iterator i = time_entries.begin(); - i != time_entries.end(); - i++) - if (event.account == (*i).account) - throw_(parse_error, "Cannot double check-in to the same account"); - - time_entries.push_back(event); + builder.push_attr(TIME_ATTR, date); + builder.push_attr(ACCOUNT_ATTR, p); + builder.begin_node(CHECKIN_NODE); + builder.append_text(n); + builder.end_node(CHECKIN_NODE, end_of_line); 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); + case 'O': { + string date(line, 2, 19); - char * p = skip_ws(line + 22); - char * n = next_element(p, true); + char * p = skip_ws(line + 22); + char * n = next_element(p, true); - clock_out_from_timelog - (parse_datetime(date), - p ? account_stack.front()->find_account(p) : NULL, n, journal); - count++; - } + builder.push_attr(TIME_ATTR, date); + builder.push_attr(ACCOUNT_ATTR, p); + builder.begin_node(CHECKIN_NODE); + builder.append_text(n); + builder.end_node(CHECKIN_NODE, end_of_line); break; -#endif // TIMELOG_SUPPORT + } - case 'D': { // a default commodity for "entry" - amount_t amt(skip_ws(line + 1)); - amount_t::current_pool->default_commodity = &amt.commodity(); + case 'D': { // specifies default commodity flags + builder.push_attr(TEMPLATE_ATTR, skip_ws(line + 1)); + builder.push_node(COMMODITY_TEMPLATE_NODE, end_of_line); break; } case 'A': // a default account for unbalanced xacts - journal->basket = - account_stack.front()->find_account(skip_ws(line + 1)); + builder.push_attr(NAME_ATTR, skip_ws(line + 1)); + builder.push_node(DEFAULT_ACCOUNT_NODE, end_of_line); break; case 'C': // a set of conversions if (char * p = std::strchr(line + 1, '=')) { *p++ = '\0'; - amount_t::parse_conversion(line + 1, p); + builder.push_attr(FROM_ATTR, skip_ws(line + 1)); + builder.push_attr(TO_ATTR, p); + builder.push_node(COMMODITY_CONVERSION_NODE, end_of_line); + } else { + throw_(parse_error, "Conversion entry (code C) must follow the format X=Y"); } 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; + if (! time_field_ptr) + throw_(parse_error, "Pricing entry (code P) is missing arguments"); string date_field = date_field_ptr; char * symbol_and_price; @@ -770,212 +391,102 @@ unsigned int textual_parser_t::parse(std::istream& in, 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); + if (! symbol_and_price) + throw_(parse_error, "Pricing entry (code P) is missing a symbol name"); } else { symbol_and_price = time_field_ptr; - datetime = parse_datetime(date_field); } + builder.push_attr(DATE_ATTR, date_field_ptr); + builder.push_attr(TIME_ATTR, time_field_ptr); + string symbol; - parse_symbol(symbol_and_price, symbol); - amount_t price(symbol_and_price); + commodity_t::parse_symbol(symbol_and_price, symbol); - if (commodity_t * commodity = - amount_t::current_pool->find_or_create(symbol)) - commodity->add_price(datetime, price); + builder.push_attr(SYMBOL_ATTR, symbol); + builder.push_attr(PRICE_ATTR, skip_ws(symbol_and_price)); + builder.push_node(PRICE_HISTORY_NODE, end_of_line); break; } case 'N': { // don't download prices char * p = skip_ws(line + 1); + string symbol; - parse_symbol(p, symbol); + commodity_t::parse_symbol(p, symbol); - if (commodity_t * commodity = - amount_t::current_pool->find_or_create(symbol)) - commodity->add_flags(COMMODITY_STYLE_NOMARKET); + builder.push_attr(SYMBOL_ATTR, symbol); + builder.push_node(COMMODITY_NOMARKET_NODE, end_of_line); break; } case 'Y': // set current year -#if 0 - // jww (2007-04-18): Need to set this up again - date_t::current_year = lexical_cast<int>(skip_ws(line + 1)); -#endif + builder.push_attr(YEAR_ATTR, skip_ws(line + 1)); + builder.push_node(CURRENT_YEAR_NODE, end_of_line); break; -#ifdef TIMELOG_SUPPORT case 'h': case 'b': -#endif case ';': // comment + // jww (2007-05-12): Read in the comment and save it break; + case '@': + case '!': { // directive + char * p = next_element(line); + string word(line + 1); + + builder.push_attr(NAME_ATTR, word); + builder.push_attr(ARG_ATTR, p); + builder.push_node(DIRECTIVE_NODE, end_of_line); + break; + } + case '-': // option setting throw_(parse_error, "Option settings are not allowed in journal files"); case '=': { // automated entry - if (! added_auto_entry_hook) { - journal->add_entry_finalizer(&auto_entry_finalizer); - added_auto_entry_hook = true; - } + builder.begin_node(AUTO_ENTRY_NODE); + builder.begin_node(RULE_NODE); + builder.append_text(skip_ws(line + 1)); + builder.end_node(RULE_NODE); - std::auto_ptr<auto_entry_t> ae(new auto_entry_t(skip_ws(line + 1))); - if (parse_transactions(in, journal, account_stack.front(), *ae, - "automated", end_pos)) { - ae->src_idx = src_idx; - ae->beg_pos = beg_pos; - ae->beg_line = beg_line; - ae->end_pos = end_pos; - ae->end_line = linenum; - journal->auto_entries.push_back(ae.release()); - } - break; - } + builder.set_position(end_of_line); - case '~': { // period entry - std::auto_ptr<period_entry_t> pe(new period_entry_t(skip_ws(line + 1))); - if (! pe->period) - throw_(parse_error, string("Parsing time period '") + skip_ws(line + 1) + "'"); - - if (parse_transactions(in, journal, account_stack.front(), *pe, - "period", end_pos)) { - if (pe->finalize()) { - extend_entry_base(journal, *pe, true); - pe->src_idx = src_idx; - pe->beg_pos = beg_pos; - pe->beg_line = beg_line; - pe->end_pos = end_pos; - pe->end_line = linenum; - journal->period_entries.push_back(pe.release()); - } else { - throw_(parse_error, "Period entry failed to balance"); - } - } - break; - } + if (! parse_transactions(in, builder)) + throw_(parse_error, "Automated entry has no transactions"); - case '@': - case '!': { // directive - char * p = next_element(line); - string word(line + 1); - if (word == "include") { - scoped_variable<path> save_path(pathname); - scoped_variable<unsigned int> save_src_idx(src_idx); - scoped_variable<unsigned long> save_beg_pos(beg_pos); - scoped_variable<unsigned long> save_end_pos(end_pos); - scoped_variable<unsigned int> save_linenum(linenum); - - if (*p != '~' && *p != '/') - pathname = (pathname.branch_path() / path(p)).normalize(); - else - pathname = resolve_path(p); - - DEBUG("ledger.textual.include", "Line " << linenum << ": " << - "Including path '" << pathname.string() << "'"); - - scoped_execute<void> - pop_include_stack(boost::bind(&include_stack_t::pop_back, - boost::ref(include_stack))); - include_stack.push_back - (std::pair<path, int>(journal->sources.back(), linenum - 1)); - - count += journal->session->read_journal(pathname, journal, - account_stack.front()); - } - else if (word == "account") { - if (account_t * acct = account_stack.front()->find_account(p)) - account_stack.push_front(acct); - else - ; // jww (2007-04-30): throw an error here - } - 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 transaction - // parser to resolve alias references. - if (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 { - ; // jww (2007-04-30): throw an error here - } - } - } - else if (word == "def" || word == "eval") { - // jww (2006-09-13): Read the string after and evaluate it. - // But also keep a list of these value expressions, and a - // way to know where they fall in the transaction sequence. - // This will be necessary so that binary file reading can - // re-evaluate them at the appopriate time. - - // compile(&journal->defs); - } + builder.end_node(AUTO_ENTRY_NODE); break; } - default: { - TRACE_START(entries, 1, "Time spent handling entries:"); + case '~': // period entry + builder.begin_node(PERIOD_ENTRY_NODE); + builder.begin_node(PERIOD_NODE); + builder.append_text(skip_ws(line + 1)); + builder.end_node(PERIOD_NODE); - std::auto_ptr<entry_t> entry - (parse_entry(in, line, journal, account_stack.front(), - *this, end_pos)); - if (entry.get()) { - entry->src_idx = src_idx; - entry->beg_pos = beg_pos; - entry->beg_line = beg_line; - entry->end_pos = end_pos; - entry->end_line = linenum; - - if (journal->add_entry(entry.get())) { - entry.release(); - count++; - } else { - throw_(parse_error, "Entry does not balance"); - } - } else { - throw_(parse_error, "Failed to parse entry"); - } + builder.set_position(end_of_line); + + if (! parse_transactions(in, builder)) + throw_(parse_error, "Repeating entry has no transactions"); + builder.end_node(PERIOD_ENTRY_NODE); + break; + + default: + TRACE_START(entries, 1, "Time spent handling entries:"); + parse_entry(in, builder, line, end_of_line); TRACE_STOP(entries, 1); break; } - } - - POP_CONTEXT(file_context(pathname, beg_line, linenum, - beg_pos, end_pos)); - beg_pos = end_pos; - beg_line = linenum; + POP_CONTEXT(builder_context(builder)); } - if (! time_entries.empty()) { - for (std::list<time_entry_t>::iterator i = time_entries.begin(); - i != time_entries.end(); - i++) - clock_out_from_timelog(now, (*i).account, NULL, journal); - time_entries.clear(); - } - - if (added_auto_entry_hook) - journal->remove_entry_finalizer(&auto_entry_finalizer); + builder.end_node(JOURNAL_NODE); TRACE_STOP(parsing_total, 1); - - return count; } } // namespace ledger |