summaryrefslogtreecommitdiff
path: root/textual.cc
diff options
context:
space:
mode:
Diffstat (limited to 'textual.cc')
-rw-r--r--textual.cc597
1 files changed, 597 insertions, 0 deletions
diff --git a/textual.cc b/textual.cc
new file mode 100644
index 00000000..783cc5de
--- /dev/null
+++ b/textual.cc
@@ -0,0 +1,597 @@
+#include "journal.h"
+#include "textual.h"
+#include "datetime.h"
+#include "valexpr.h"
+#include "error.h"
+#include "option.h"
+#include "config.h"
+#include "timing.h"
+#include "util.h"
+#ifdef USE_BOOST_PYTHON
+#include "py_eval.h"
+#endif
+
+#include <fstream>
+#include <sstream>
+#include <cstring>
+#include <ctime>
+#include <cctype>
+
+#define TIMELOG_SUPPORT 1
+
+namespace ledger {
+
+#define MAX_LINE 1024
+
+static std::string path;
+static unsigned int linenum;
+
+#ifdef TIMELOG_SUPPORT
+static std::time_t time_in;
+static account_t * last_account;
+static std::string last_desc;
+#endif
+
+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;
+}
+
+void parse_amount(const char * text, amount_t& amt, unsigned short flags,
+ transaction_t& xact)
+{
+ if (*text != '(') {
+ amt.parse(text, flags);
+ } else {
+ value_expr_t * expr = parse_value_expr(text);
+ value_t result;
+ expr->compute(result, details_t(xact));
+ switch (result.type) {
+ case value_t::BOOLEAN:
+ amt = *((bool *) result.data);
+ break;
+ case value_t::INTEGER:
+ amt = *((long *) result.data);
+ break;
+ case value_t::AMOUNT:
+ amt = *((amount_t *) result.data);
+ break;
+
+ case value_t::BALANCE:
+ case value_t::BALANCE_PAIR:
+ throw parse_error(path, linenum, "Value expression yields a balance");
+ break;
+ }
+ }
+}
+
+transaction_t * parse_transaction(char * line, account_t * account)
+{
+ // The account will be determined later...
+
+ std::auto_ptr<transaction_t> xact(new transaction_t(NULL));
+
+ // The call to `next_element' will skip past the account name,
+ // and return a pointer to the beginning of the amount. Once
+ // we know where the amount is, we can strip off any
+ // transaction note, and parse it.
+
+ char * p = skip_ws(line);
+ if (char * cost_str = next_element(p, true)) {
+ cost_str = skip_ws(cost_str);
+ bool has_amount = *cost_str;
+
+ if (char * note_str = std::strchr(cost_str, ';')) {
+ if (cost_str == note_str)
+ has_amount = false;
+ *note_str++ = '\0';
+ xact->note = skip_ws(note_str);
+ }
+
+ if (has_amount) {
+ bool per_unit = true;
+ char * price_str = std::strchr(cost_str, '@');
+ if (price_str) {
+ if (price_str == cost_str)
+ throw parse_error(path, linenum, "Cost specified without amount");
+
+ *price_str++ = '\0';
+ if (*price_str == '@') {
+ per_unit = false;
+ price_str++;
+ }
+ }
+ parse_amount(skip_ws(cost_str), xact->amount, AMOUNT_PARSE_NO_REDUCE,
+ *xact);
+ if (price_str) {
+ xact->cost = new amount_t;
+ parse_amount(skip_ws(price_str), *xact->cost, AMOUNT_PARSE_NO_MIGRATE,
+ *xact);
+ }
+
+ if (price_str && per_unit) {
+ *xact->cost *= xact->amount;
+ *xact->cost = xact->cost->round(xact->cost->commodity().precision);
+ }
+ xact->amount.reduce();
+ }
+ }
+
+ if (*p == '[' || *p == '(') {
+ xact->flags |= TRANSACTION_VIRTUAL;
+ if (*p == '[')
+ xact->flags |= TRANSACTION_BALANCE;
+ p++;
+
+ char * e = p + (std::strlen(p) - 1);
+ assert(*e == ')' || *e == ']');
+ *e = '\0';
+ }
+
+ xact->account = account->find_account(p);
+
+ return xact.release();
+}
+
+bool parse_transactions(std::istream& in,
+ account_t * account,
+ entry_base_t& entry,
+ const std::string& kind)
+{
+ 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;
+ linenum++;
+ if (line[0] == ' ' || line[0] == '\t' || line[0] == '\r') {
+ char * p = skip_ws(line);
+ if (! *p || *p == '\r')
+ break;
+ }
+ if (transaction_t * xact = parse_transaction(line, account)) {
+ entry.add_transaction(xact);
+ added = true;
+ }
+ }
+
+ return added;
+}
+
+namespace {
+ TIMER_DEF(entry_finish, "finalizing entry");
+ TIMER_DEF(entry_xacts, "parsing transactions");
+ TIMER_DEF(entry_details, "parsing entry details");
+ TIMER_DEF(entry_date, "parsing entry date");
+}
+
+entry_t * parse_entry(std::istream& in, char * line, account_t * master,
+ textual_parser_t& parser)
+{
+ std::auto_ptr<entry_t> curr(new entry_t);
+
+ // Parse the date
+
+ TIMER_START(entry_date);
+
+ char * next = next_element(line);
+
+ if (! quick_parse_date(line, &curr->date))
+ throw parse_error(path, linenum, "Failed to parse date");
+
+ TIMER_STOP(entry_date);
+
+ // Parse the optional cleared flag: *
+
+ TIMER_START(entry_details);
+
+ if (next && *next == '*') {
+ curr->state = entry_t::CLEARED;
+ next = skip_ws(++next);
+ }
+
+ // Parse the optional code: (TEXT)
+
+ if (next && *next == '(') {
+ if (char * p = std::strchr(next++, ')')) {
+ *p++ = '\0';
+ curr->code = next;
+ next = skip_ws(p);
+ }
+ }
+
+ // Parse the description text
+
+ curr->payee = next ? next : "<Unspecified payee>";
+
+ TIMER_STOP(entry_details);
+
+ // Parse all of the transactions associated with this entry
+
+ TIMER_START(entry_xacts);
+
+ while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) {
+ in.getline(line, MAX_LINE);
+ if (in.eof())
+ break;
+ linenum++;
+ if (line[0] == ' ' || line[0] == '\t' || line[0] == '\r') {
+ char * p = skip_ws(line);
+ if (! *p || *p == '\r')
+ break;
+ }
+ if (transaction_t * xact = parse_transaction(line, master))
+ curr->add_transaction(xact);
+ }
+
+ TIMER_STOP(entry_xacts);
+
+ return curr.release();
+}
+
+template <typename T>
+struct push_var {
+ T& var;
+ T prev;
+ push_var(T& _var) : var(_var), prev(var) {}
+ ~push_var() { var = prev; }
+};
+
+static inline void parse_symbol(char *& p, std::string& symbol)
+{
+ if (*p == '"') {
+ char * q = std::strchr(p + 1, '"');
+ if (! q)
+ throw parse_error(path, linenum,
+ "Quoted commodity symbol lacks closing quote");
+ symbol = std::string(p + 1, 0, q - p - 1);
+ p = q + 2;
+ } else {
+ char * q = std::strchr(p, ' ');
+ if (q) {
+ *q = '\0';
+ symbol = std::string(p, 0, q - p);
+ p = q + 1;
+ } else {
+ symbol = p;
+ p += symbol.length();
+ }
+ }
+ if (symbol.empty())
+ throw parse_error(path, linenum, "Failed to parse commodity");
+}
+
+unsigned int textual_parser_t::parse(std::istream& in,
+ journal_t * journal,
+ account_t * master,
+ const std::string * original_file)
+{
+ static bool added_auto_entry_hook = false;
+ static char line[MAX_LINE + 1];
+ char c;
+ unsigned int count = 0;
+ unsigned int errors = 0;
+
+ std::list<account_t *> account_stack;
+ auto_entry_finalizer_t auto_entry_finalizer(journal);
+
+ if (! master)
+ master = journal->master;
+
+ account_stack.push_front(master);
+
+ path = journal->sources.back();
+ linenum = 1;
+
+ while (in.good() && ! in.eof()) {
+ try {
+ in.getline(line, MAX_LINE);
+ if (in.eof())
+ break;
+ linenum++;
+
+ switch (line[0]) {
+ case '\0':
+ case '\r':
+ break;
+
+ case ' ':
+ case '\t': {
+ char * p = skip_ws(line);
+ if (*p && *p != '\r')
+ throw parse_error(path, linenum - 1, "Line begins with whitespace");
+ break;
+ }
+
+#ifdef TIMELOG_SUPPORT
+ case 'i':
+ case 'I': {
+ std::string date(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ char * n = next_element(p, true);
+ last_desc = n ? n : "";
+
+ struct std::tm when;
+ if (strptime(date.c_str(), "%Y/%m/%d %H:%M:%S", &when)) {
+ time_in = std::mktime(&when);
+ last_account = account_stack.front()->find_account(p);
+ } else {
+ last_account = NULL;
+ throw parse_error(path, linenum, "Cannot parse timelog entry date");
+ }
+ break;
+ }
+
+ case 'o':
+ case 'O':
+ if (last_account) {
+ std::string date(line, 2, 19);
+
+ char * p = skip_ws(line + 22);
+ if (last_desc.empty() && *p)
+ last_desc = p;
+
+ struct std::tm when;
+ if (strptime(date.c_str(), "%Y/%m/%d %H:%M:%S", &when)) {
+ std::auto_ptr<entry_t> curr(new entry_t);
+ curr->date = std::mktime(&when);
+ curr->state = entry_t::CLEARED;
+ curr->code = "";
+ curr->payee = last_desc;
+
+ double diff = std::difftime(curr->date, time_in);
+ char buf[32];
+ std::sprintf(buf, "%lds", long(diff));
+ amount_t amt;
+ amt.parse(buf);
+
+ transaction_t * xact
+ = new transaction_t(last_account, amt, TRANSACTION_VIRTUAL);
+ curr->add_transaction(xact);
+
+ if (! journal->add_entry(curr.get()))
+ throw parse_error(path, linenum,
+ "Failed to record 'out' timelog entry");
+ else
+ curr.release();
+
+ count++;
+ } else {
+ throw parse_error(path, linenum, "Cannot parse timelog entry date");
+ }
+
+ last_account = NULL;
+ }
+ break;
+#endif // TIMELOG_SUPPORT
+
+ case 'C': // a set of conversions
+ if (char * p = std::strchr(line + 1, '=')) {
+ *p++ = '\0';
+ parse_conversion(line + 1, p);
+ }
+ break;
+
+ case 'P': { // a pricing entry
+ std::time_t date;
+
+ char * b = skip_ws(line + 1);
+ char * p = std::strchr(b, ' ');
+ if (! p) break;
+ *p++ = '\0';
+
+ if (! quick_parse_date(b, &date))
+ throw parse_error(path, linenum, "Failed to parse date");
+
+ int hour, min, sec;
+
+ if (! *p) break; hour = (*p++ - '0') * 10;
+ if (! *p) break; hour += *p++ - '0';
+ p++; if (! *p) break;
+ if (! *p) break; min = (*p++ - '0') * 10;
+ if (! *p) break; min += *p++ - '0';
+ p++; if (! *p) break;
+ if (! *p) break; sec = (*p++ - '0') * 10;
+ if (! *p) break; sec += *p++ - '0';
+ p++; if (! *p) break;
+
+ date = std::time_t(((unsigned long) date) +
+ hour * 3600 + min * 60 + sec);
+
+ std::string symbol;
+ amount_t price;
+
+ parse_symbol(p, symbol);
+ price.parse(skip_ws(p));
+
+ commodity_t * commodity = commodity_t::find_commodity(symbol, true);
+ commodity->add_price(date, price);
+ break;
+ }
+
+ case 'N': { // don't download prices
+ char * p = skip_ws(line + 1);
+ std::string symbol;
+ parse_symbol(p, symbol);
+
+ commodity_t * commodity = commodity_t::find_commodity(symbol, true);
+ commodity->flags |= COMMODITY_STYLE_NOMARKET;
+ break;
+ }
+
+ case 'Y': // set the current year
+ now_year = std::atoi(skip_ws(line + 1)) - 1900;
+ break;
+
+#ifdef TIMELOG_SUPPORT
+ case 'h':
+ case 'b':
+#endif
+ case ';': // a comment line
+ break;
+
+ case '-': { // option setting
+ char * p = std::strchr(line, ' ');
+ if (! p)
+ p = std::strchr(line, '=');
+ if (p)
+ *p++ = '\0';
+ process_option(config_options, line + 2, p ? skip_ws(p) : NULL);
+ 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_transactions(in, account_stack.front(), *ae, "automated")) {
+ if (ae->finalize())
+ journal->auto_entries.push_back(ae);
+ else
+ throw parse_error(path, linenum,
+ "Automated entry failed to balance");
+ }
+ break;
+ }
+
+ case '~': { // period entry
+ period_entry_t * pe = new period_entry_t(skip_ws(line + 1));
+ if (! pe->period)
+ throw parse_error(path, linenum,
+ std::string("Parsing time period '") + line + "'");
+
+ if (parse_transactions(in, account_stack.front(), *pe, "period")) {
+ if (pe->finalize()) {
+ extend_entry_base(journal, *pe);
+ journal->period_entries.push_back(pe);
+ } else {
+ throw parse_error(path, linenum, "Period entry failed to balance");
+ }
+ }
+ break;
+ }
+
+ case '!': { // directive
+ char * p = std::strchr(line, ' ');
+ if (p)
+ *p++ = '\0';
+ std::string word(line + 1);
+ if (word == "include") {
+ push_var<unsigned int> save_linenum(linenum);
+ push_var<std::string> save_path(path);
+
+ path = skip_ws(p);
+ if (path[0] != '/' && path[0] != '\\') {
+ std::string::size_type pos = save_path.prev.rfind('/');
+ if (pos == std::string::npos)
+ pos = save_path.prev.rfind('\\');
+ if (pos != std::string::npos)
+ path = std::string(save_path.prev, 0, pos + 1) + path;
+ }
+
+ DEBUG_PRINT("ledger.textual.include",
+ "Including path '" << path << "'");
+ count += parse_journal_file(path, journal, account_stack.front());
+ }
+ else if (word == "account") {
+ account_t * acct;
+ acct = account_stack.front()->find_account(skip_ws(p));
+ account_stack.push_front(acct);
+ }
+ else if (word == "end") {
+ account_stack.pop_front();
+ }
+#ifdef USE_BOOST_PYTHON
+ else if (word == "python") {
+ python_eval(in, PY_EVAL_MULTI);
+ }
+#endif
+ break;
+ }
+
+ default: {
+ unsigned int first_line = linenum;
+ if (entry_t * entry = parse_entry(in, line, account_stack.front(),
+ *this)) {
+ if (journal->add_entry(entry)) {
+ count++;
+ } else {
+ delete entry;
+ throw parse_error(path, first_line, "Entry does not balance");
+ }
+ } else {
+ throw parse_error(path, first_line, "Failed to parse entry");
+ }
+ break;
+ }
+ }
+ }
+ catch (const parse_error& err) {
+ std::cerr << "Error: " << err.what() << std::endl;
+ errors++;
+ }
+ catch (const amount_error& err) {
+ std::cerr << "Error: " << path << ", line " << (linenum - 1) << ": "
+ << err.what() << std::endl;;
+ errors++;
+ }
+ catch (const error& err) {
+ std::cerr << "Error: " << path << ", line " << (linenum - 1) << ": "
+ << err.what() << std::endl;;
+ errors++;
+ }
+ }
+
+ done:
+ if (added_auto_entry_hook)
+ journal->remove_entry_finalizer(&auto_entry_finalizer);
+
+ if (errors > 0)
+ throw error(std::string("Errors parsing file '") + path + "'");
+
+ return count;
+}
+
+} // namespace ledger
+
+#ifdef USE_BOOST_PYTHON
+
+#include <boost/python.hpp>
+
+using namespace boost::python;
+using namespace ledger;
+
+BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(textual_parse_overloads,
+ textual_parser_t::parse, 2, 4)
+
+void export_textual() {
+ class_< textual_parser_t, bases<parser_t> > ("TextualParser")
+ .def("test", &textual_parser_t::test)
+ .def("parse", &textual_parser_t::parse, textual_parse_overloads())
+ ;
+}
+
+#endif // USE_BOOST_PYTHON