diff options
Diffstat (limited to 'parse.cc')
-rw-r--r-- | parse.cc | 582 |
1 files changed, 582 insertions, 0 deletions
diff --git a/parse.cc b/parse.cc new file mode 100644 index 00000000..e4850973 --- /dev/null +++ b/parse.cc @@ -0,0 +1,582 @@ +#include "ledger.h" + +#include <fstream> +#include <cstring> +#include <ctime> +#include <cctype> + +#define TIMELOG_SUPPORT 1 + +namespace ledger { + +static inline char * skip_ws(char * ptr) +{ + while (std::isspace(*ptr)) + ptr++; + return ptr; +} + +static 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 const char *formats[] = { + "%Y-%m-%d", + "%m-%d", + "%Y/%m/%d", + "%m/%d", + "%Y.%m.%d", + "%m.%d", + "%a", + "%A", + "%b", + "%B", + "%Y", + NULL +}; + +bool parse_date_mask(const char * date_str, struct std::tm * result) +{ + for (const char ** f = formats; *f; f++) { + memset(result, INT_MAX, sizeof(struct std::tm)); + if (strptime(date_str, *f, result)) + return true; + } + return false; +} + +bool parse_date(const char * date_str, std::time_t * result, const int year) +{ + struct std::tm when; + + if (! parse_date_mask(date_str, &when)) + return false; + + static std::time_t now = std::time(NULL); + static struct std::tm * now_tm = std::localtime(&now); + + when.tm_hour = 0; + when.tm_min = 0; + when.tm_sec = 0; + + if (when.tm_year == -1) + when.tm_year = ((year == -1) ? now_tm->tm_year : (year - 1900)); + + if (when.tm_mon == -1) + when.tm_mon = now_tm->tm_mon; + + if (when.tm_mday == -1) + when.tm_mday = now_tm->tm_mday; + + *result = std::mktime(&when); + + return true; +} + +void record_price(const std::string& symbol, amount * price, + std::time_t * date = NULL) +{ + commodity * comm = NULL; + commodities_map_iterator item = main_ledger->commodities.find(symbol); + if (item == main_ledger->commodities.end()) + comm = new commodity(symbol); + else + comm = (*item).second; + + assert(comm); + comm->set_price(price, date); +} + +void parse_price_setting(const std::string& setting) +{ + char buf[128]; + std::strcpy(buf, setting.c_str()); + + assert(setting.length() < 128); + + char * c = buf; + char * p = std::strchr(buf, '='); + if (! p) { + std::cerr << "Warning: Invalid price setting: " << setting << std::endl; + } else { + *p++ = '\0'; + record_price(c, create_amount(p)); + } +} + +#define MAX_LINE 1024 + + int linenum; +static bool do_compute; +static std::string account_prefix; + +transaction * parse_transaction_text(char * line, book * ledger) +{ + transaction * xact = new transaction(); + + // 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); + char * cost_str = next_element(p, true); + char * note_str; + + // If there is no amount given, it is intended as an implicit + // amount; we must use the opposite of the value of the + // preceding transaction. + + if (! cost_str || ! *cost_str || *cost_str == ';') { + if (cost_str && *cost_str) { + while (*cost_str == ';' || std::isspace(*cost_str)) + cost_str++; + xact->note = cost_str; + } + + xact->cost = NULL; + } + else { + note_str = std::strchr(cost_str, ';'); + if (note_str) { + *note_str++ = '\0'; + xact->note = skip_ws(note_str); + } + + for (char * t = cost_str + (std::strlen(cost_str) - 1); + std::isspace(*t); + t--) + *t = '\0'; + + xact->cost = create_amount(cost_str); + } + + if (*p == '[' || *p == '(') { + xact->is_virtual = true; + xact->specified = true; + xact->must_balance = *p == '['; + p++; + + char * e = p + (std::strlen(p) - 1); + assert(*e == ')' || *e == ']'); + *e = '\0'; + } + + std::string name = account_prefix + p; + xact->acct = ledger->find_account(name.c_str()); + + if (do_compute && xact->cost) + xact->acct->balance.credit(xact->cost); + + return xact; +} + +transaction * parse_transaction(std::istream& in, book * ledger) +{ + static char line[MAX_LINE + 1]; + in.getline(line, MAX_LINE); + linenum++; + + return parse_transaction_text(line, ledger); +} + +entry * parse_entry(std::istream& in, book * ledger) +{ + entry * curr = new entry(ledger); + + static char line[MAX_LINE + 1]; + in.getline(line, MAX_LINE); + linenum++; + + // Parse the date + + char * next = next_element(line); + if (! parse_date(line, &curr->date, ledger->current_year)) { + std::cerr << "Error, line " << linenum + << ": Failed to parse date: " << line << std::endl; + return NULL; + } + + // Parse the optional cleared flag: * + + if (*next == '*') { + curr->cleared = true; + next = skip_ws(++next); + } + + // Parse the optional code: (TEXT) + + if (*next == '(') { + if (char * p = std::strchr(next++, ')')) { + *p++ = '\0'; + curr->code = next; + next = skip_ws(p); + } + } + + // Parse the description text + + curr->desc = next; + + // Parse all of the transactions associated with this entry + + while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) + if (transaction * xact = parse_transaction(in, ledger)) + curr->xacts.push_back(xact); + + // If there were no transactions, throw away the entry + + if (curr->xacts.empty() || ! curr->finalize(do_compute)) { + delete curr; + return NULL; + } + + return curr; +} + +void parse_automated_transactions(std::istream& in, book * ledger) +{ + static char line[MAX_LINE + 1]; + + regexps_list * masks = NULL; + + while (! in.eof() && in.peek() == '=') { + in.getline(line, MAX_LINE); + linenum++; + + char * p = line + 1; + p = skip_ws(p); + + if (! masks) + masks = new regexps_list; + masks->push_back(mask(p)); + } + + std::list<transaction *> * xacts = NULL; + + while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) { + if (transaction * xact = parse_transaction(in, ledger)) { + if (! xacts) + xacts = new std::list<transaction *>; + + if (! xact->cost) { + std::cerr << "Error, line " << (linenum - 1) + << ": All automated transactions must have a value." + << std::endl; + } else { + xacts->push_back(xact); + } + } + } + + if (masks && xacts) + ledger->virtual_mapping.insert(book::virtual_map_pair(masks, xacts)); + else if (masks) + delete masks; + else if (xacts) + delete xacts; +} + +////////////////////////////////////////////////////////////////////// +// +// Ledger parser +// + +#ifdef TIMELOG_SUPPORT +static std::time_t time_in; +static account * last_account; +static std::string last_desc; +#endif + +int parse_ledger(book * ledger, std::istream& in, + regexps_list& regexps, bool compute_balances, + const char * acct_prefix) +{ + static char line[MAX_LINE + 1]; + char c; + int count = 0; + std::string old_account_prefix = account_prefix; + + linenum = 1; + do_compute = compute_balances; + if (acct_prefix) { + account_prefix += acct_prefix; + account_prefix += ":"; + } + + while (! in.eof()) { + switch (in.peek()) { + case -1: // end of file + goto done; + + case '\n': + linenum++; + case '\r': // skip blank lines + in.get(c); + break; + +#ifdef TIMELOG_SUPPORT + case 'i': + case 'I': { + std::string date, time; + + in >> c; + in >> date; + in >> time; + date += " "; + date += time; + + in.getline(line, MAX_LINE); + linenum++; + + char * p = skip_ws(line); + char * n = next_element(p, true); + last_desc = n ? n : ""; + + static struct std::tm when; + if (strptime(date.c_str(), "%Y/%m/%d %H:%M:%S", &when)) { + time_in = std::mktime(&when); + last_account = ledger->find_account(p); + } else { + std::cerr << "Error, line " << (linenum - 1) + << ": Cannot parse timelog entry date." + << std::endl; + last_account = NULL; + } + break; + } + + case 'o': + case 'O': + if (last_account) { + std::string date, time; + + in >> c; + in >> date; + in >> time; + date += " "; + date += time; + + static struct std::tm when; + if (strptime(date.c_str(), "%Y/%m/%d %H:%M:%S", &when)) { + entry * curr = new entry(ledger); + + curr->date = std::mktime(&when); + + double diff = (curr->date - time_in) / 60.0 / 60.0; + char buf[128]; + std::sprintf(buf, "%fh", diff); + + curr->cleared = true; + curr->code = ""; + curr->desc = last_desc; + + std::string xact_line = "("; + xact_line += last_account->as_str(); + xact_line += ") "; + xact_line += buf; + + std::strcpy(buf, xact_line.c_str()); + + if (transaction * xact = parse_transaction_text(buf, ledger)) { + curr->xacts.push_back(xact); + + // Make sure numbers are reported only to 1 decimal place. + commodity * cmdty = xact->cost->commdty(); + cmdty->precision = 1; + } + + ledger->entries.push_back(curr); + count++; + } else { + std::cerr << "Error, line " << (linenum - 1) + << ": Cannot parse timelog entry date." + << std::endl; + } + + last_account = NULL; + } + break; +#endif // TIMELOG_SUPPORT + + case 'P': { // a pricing entry + in >> c; + + std::time_t date; + std::string symbol; + + in >> line; // the date + if (! parse_date(line, &date, ledger->current_year)) { + std::cerr << "Error, line " << linenum + << ": Failed to parse date: " << line << std::endl; + break; + } + + int hour, min, sec; + in >> hour; // the time + in >> c; + in >> min; + in >> c; + in >> sec; + date = std::time_t(((unsigned long) date) + + hour * 3600 + min * 60 + sec); + + in >> symbol; // the commodity + in >> line; // the price + + // Add this pricing entry to the history for the given + // commodity. + record_price(symbol, create_amount(line), &date); + break; + } + + case 'N': { // don't download prices + in >> c; + in >> line; // the symbol + + commodity * comm = NULL; + commodities_map_iterator item = main_ledger->commodities.find(line); + if (item == main_ledger->commodities.end()) + comm = new commodity(line); + else + comm = (*item).second; + + assert(comm); + if (comm) + comm->sought = true; + break; + } + + case 'C': { // a flat conversion + in >> c; + + std::string symbol; + in >> symbol; // the commodity + in >> line; // the price + + // Add this pricing entry to the given commodity + record_price(symbol, create_amount(line)); + break; + } + + case 'Y': // set the current year + in >> c; + in >> ledger->current_year; + break; + +#ifdef TIMELOG_SUPPORT + case 'h': + case 'b': +#endif + case ';': // a comment line + in.getline(line, MAX_LINE); + linenum++; + break; + + case '-': + case '+': // permanent regexps + in.getline(line, MAX_LINE); + linenum++; + + // Add the regexp to whatever masks currently exist + regexps.push_back(mask(line)); + break; + + case '=': // automated transactions + do_compute = false; + parse_automated_transactions(in, ledger); + do_compute = compute_balances; + break; + + case '!': // directive + in >> line; + if (std::string(line) == "!include") { + std::string path; + bool has_prefix = false; + + in >> path; + + if (in.peek() == ' ') { + has_prefix = true; + in.getline(line, MAX_LINE); + } + + int curr_linenum = linenum; + count += parse_ledger_file(ledger, path, regexps, compute_balances, + has_prefix ? skip_ws(line) : NULL); + linenum = curr_linenum; + } + break; + + default: + if (entry * ent = parse_entry(in, ledger)) { + ledger->entries.push_back(ent); + count++; + } + break; + } + } + + done: + account_prefix = old_account_prefix; + + return count; +} + +int parse_ledger_file(book * ledger, const std::string& file, + regexps_list& regexps, bool compute_balances, + const char * acct_prefix) +{ + std::ifstream stream(file.c_str()); + + // Parse the ledger + +#ifdef READ_GNUCASH + char buf[32]; + stream.get(buf, 31); + stream.seekg(0); + + if (std::strncmp(buf, "<?xml version=\"1.0\"?>", 21) == 0) + return parse_gnucash(ledger, stream, compute_balances); + else +#endif + return parse_ledger(ledger, stream, regexps, compute_balances, + acct_prefix); +} + +////////////////////////////////////////////////////////////////////// +// +// Read other kinds of data from files +// + +void read_regexps(const std::string& path, regexps_list& regexps) +{ + std::ifstream file(path.c_str()); + + while (! file.eof()) { + char buf[80]; + file.getline(buf, 79); + if (*buf && ! std::isspace(*buf)) + regexps.push_back(mask(buf)); + } +} + +} // namespace ledger |