double has been disabled, because I am no longer packaging the gdtoa library with Ledger (because double conversion really has nothing to do with what Ledger does). If you wish to use it, you can find gdtoa in cpp-rewrite-2006, under a sub-directory of the same name.
1035 lines
26 KiB
C++
1035 lines
26 KiB
C++
#if defined(__GNUG__) && __GNUG__ < 3
|
|
#define _XOPEN_SOURCE
|
|
#endif
|
|
|
|
#include "journal.h"
|
|
#include "textual.h"
|
|
#include "valexpr.h"
|
|
#include "parsexp.h"
|
|
#include "utils.h"
|
|
#include "acconf.h"
|
|
|
|
#define TIMELOG_SUPPORT 1
|
|
|
|
namespace ledger {
|
|
|
|
#define MAX_LINE 1024
|
|
|
|
static path pathname;
|
|
static unsigned int linenum;
|
|
static unsigned int src_idx;
|
|
static accounts_map account_aliases;
|
|
|
|
static std::list<std::pair<path, int> > include_stack;
|
|
|
|
#ifdef TIMELOG_SUPPORT
|
|
struct time_entry_t
|
|
{
|
|
datetime_t checkin;
|
|
account_t * account;
|
|
string desc;
|
|
|
|
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);
|
|
}
|
|
};
|
|
#endif
|
|
|
|
namespace {
|
|
value_expr parse_amount_expr(std::istream& in,
|
|
amount_t& amount,
|
|
transaction_t * xact,
|
|
unsigned short flags = 0)
|
|
{
|
|
value_expr expr =
|
|
value_expr::parser->parse(in, flags |
|
|
EXPR_PARSE_RELAXED | EXPR_PARSE_PARTIAL);
|
|
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Parsed an amount expression");
|
|
|
|
#ifdef DEBUG_ENABLED
|
|
DEBUG_IF("ledger.textual.parse") {
|
|
if (_debug_stream) {
|
|
ledger::dump_value_expr(*_debug_stream, expr);
|
|
*_debug_stream << std::endl;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (expr) {
|
|
if (! expr::compute_amount(expr, amount, xact))
|
|
throw new parse_error("Amount expression failed to compute");
|
|
|
|
#if 0
|
|
if (expr->kind == expr::node_t::VALUE) {
|
|
expr = NULL;
|
|
} else {
|
|
DEBUG_IF("ledger.textual.parse") {
|
|
std::cout << "Value expression tree:" << std::endl;
|
|
ledger::dump_value_expr(std::cout, expr.get());
|
|
}
|
|
}
|
|
#else
|
|
expr = value_expr();
|
|
#endif
|
|
}
|
|
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"The transaction amount is " << xact->amount);
|
|
return expr;
|
|
}
|
|
}
|
|
|
|
transaction_t * parse_transaction(char * line, account_t * account,
|
|
entry_t * entry = NULL)
|
|
{
|
|
std::istringstream in(line);
|
|
|
|
string err_desc;
|
|
try {
|
|
|
|
// The account will be determined later...
|
|
std::auto_ptr<transaction_t> xact(new transaction_t(NULL));
|
|
if (entry)
|
|
xact->entry = entry;
|
|
|
|
// Parse the state flag
|
|
|
|
char p = peek_next_nonws(in);
|
|
switch (p) {
|
|
case '*':
|
|
xact->state = transaction_t::CLEARED;
|
|
in.get(p);
|
|
p = peek_next_nonws(in);
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Parsed the CLEARED flag");
|
|
break;
|
|
case '!':
|
|
xact->state = transaction_t::PENDING;
|
|
in.get(p);
|
|
p = peek_next_nonws(in);
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Parsed the PENDING flag");
|
|
break;
|
|
}
|
|
|
|
// Parse the account name
|
|
|
|
unsigned long account_beg = in.tellg();
|
|
unsigned long account_end = account_beg;
|
|
while (! in.eof()) {
|
|
in.get(p);
|
|
if (in.eof() || (std::isspace(p) &&
|
|
(p == '\t' || in.peek() == EOF ||
|
|
std::isspace(in.peek()))))
|
|
break;
|
|
account_end++;
|
|
}
|
|
|
|
if (account_beg == account_end)
|
|
throw new parse_error("No account was specified");
|
|
|
|
char * b = &line[account_beg];
|
|
char * e = &line[account_end];
|
|
if ((*b == '[' && *(e - 1) == ']') ||
|
|
(*b == '(' && *(e - 1) == ')')) {
|
|
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");
|
|
}
|
|
b++; e--;
|
|
}
|
|
|
|
string name(b, e - b);
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Parsed account name " << name);
|
|
if (account_aliases.size() > 0) {
|
|
accounts_map::const_iterator i = account_aliases.find(name);
|
|
if (i != account_aliases.end())
|
|
xact->account = (*i).second;
|
|
}
|
|
if (! xact->account)
|
|
xact->account = account->find_account(name);
|
|
|
|
// Parse the optional amount
|
|
|
|
bool saw_amount = false;
|
|
|
|
if (in.good() && ! in.eof()) {
|
|
p = peek_next_nonws(in);
|
|
if (in.eof())
|
|
goto finished;
|
|
if (p == ';')
|
|
goto parse_note;
|
|
|
|
try {
|
|
unsigned long beg = (long)in.tellg();
|
|
|
|
xact->amount_expr =
|
|
parse_amount_expr(in, xact->amount, xact.get(),
|
|
EXPR_PARSE_NO_REDUCE);
|
|
saw_amount = true;
|
|
|
|
if (! xact->amount.is_null()) {
|
|
xact->amount.reduce();
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Reduced amount is " << xact->amount);
|
|
}
|
|
|
|
// jww (2008-07-24): I don't think this is right, since amount_expr is
|
|
// always NULL right now
|
|
if (xact->amount_expr) {
|
|
unsigned long end = (long)in.tellg();
|
|
xact->amount_expr.expr_str = string(line, beg, end - beg);
|
|
}
|
|
}
|
|
catch (error * err) {
|
|
err_desc = "While parsing transaction amount:";
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Parse the optional cost (@ PER-UNIT-COST, @@ TOTAL-COST)
|
|
|
|
if (in.good() && ! in.eof()) {
|
|
p = peek_next_nonws(in);
|
|
if (p == '@') {
|
|
if (! saw_amount)
|
|
throw new parse_error
|
|
("Transaction cannot have a cost expression with an amount");
|
|
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"Found a price indicator");
|
|
bool per_unit = true;
|
|
in.get(p);
|
|
if (in.peek() == '@') {
|
|
in.get(p);
|
|
per_unit = false;
|
|
DEBUG("ledger.textual.parse", "line " << linenum << ": " <<
|
|
"And it's for a total price");
|
|
}
|
|
|
|
if (in.good() && ! in.eof()) {
|
|
xact->cost = amount_t();
|
|
|
|
try {
|
|
unsigned long beg = (long)in.tellg();
|
|
|
|
if (parse_amount_expr(in, *xact->cost, xact.get(),
|
|
EXPR_PARSE_NO_MIGRATE))
|
|
throw new parse_error
|
|
("A transaction's cost must evaluate to a constant value");
|
|
assert(xact->cost->valid());
|
|
|
|
// jww (2008-07-24): I don't think this is right...
|
|
if (xact->cost_expr) {
|
|
unsigned long end = (long)in.tellg();
|
|
if (per_unit)
|
|
xact->cost_expr->expr_str = (string("@") +
|
|
string(line, beg, end - beg));
|
|
else
|
|
xact->cost_expr->expr_str = (string("@@") +
|
|
string(line, beg, end - beg));
|
|
}
|
|
}
|
|
catch (error * err) {
|
|
err_desc = "While parsing transaction cost:";
|
|
throw err;
|
|
}
|
|
|
|
if (xact->cost->sign() < 0)
|
|
throw new parse_error("A transaction's cost may not be negative");
|
|
|
|
amount_t per_unit_cost(*xact->cost);
|
|
if (per_unit)
|
|
*xact->cost *= xact->amount;
|
|
else
|
|
per_unit_cost /= xact->amount;
|
|
|
|
if (xact->amount.commodity() &&
|
|
! xact->amount.commodity().annotated)
|
|
xact->amount.annotate_commodity
|
|
(annotation_t
|
|
(per_unit_cost,
|
|
xact->entry ? optional<datetime_t>(xact->entry->actual_date()) : none,
|
|
xact->entry ? optional<string>(xact->entry->code) : none));
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the optional note
|
|
|
|
parse_note:
|
|
if (in.good() && ! in.eof()) {
|
|
p = peek_next_nonws(in);
|
|
if (p == ';') {
|
|
in.get(p);
|
|
p = peek_next_nonws(in);
|
|
xact->note = &line[in.tellg()];
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
finished:
|
|
return xact.release();
|
|
|
|
}
|
|
catch (error * err) {
|
|
err->context.push_back
|
|
(new line_context(line, (long)in.tellg() - 1,
|
|
! err_desc.empty() ?
|
|
err_desc : "While parsing transaction:"));
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
bool parse_transactions(std::istream& in,
|
|
account_t * account,
|
|
entry_base_t& entry,
|
|
const string& kind,
|
|
unsigned long beg_pos)
|
|
{
|
|
TRACE_START(entry_xacts, 1, "Time spent parsing transactions:");
|
|
|
|
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;
|
|
|
|
int len = std::strlen(line);
|
|
if (line[len - 1] == '\r')
|
|
line[--len] = '\0';
|
|
|
|
beg_pos += len + 1;
|
|
linenum++;
|
|
|
|
if (line[0] == ' ' || line[0] == '\t') {
|
|
char * p = skip_ws(line);
|
|
if (! *p)
|
|
break;
|
|
}
|
|
if (transaction_t * xact = parse_transaction(line, account)) {
|
|
entry.add_transaction(xact);
|
|
added = true;
|
|
}
|
|
}
|
|
|
|
TRACE_STOP(entry_xacts, 1);
|
|
|
|
return added;
|
|
}
|
|
|
|
entry_t * parse_entry(std::istream& in, char * line, account_t * master,
|
|
textual_parser_t& parser, unsigned long& pos)
|
|
{
|
|
TRACE_START(entry_text, 1, "Time spent preparing entry text:");
|
|
|
|
std::auto_ptr<entry_t> curr(new entry_t);
|
|
|
|
// Parse the date
|
|
|
|
char * next = next_element(line);
|
|
|
|
if (char * p = std::strchr(line, '=')) {
|
|
*p++ = '\0';
|
|
curr->_date_eff = parse_datetime(p);
|
|
}
|
|
curr->_date = parse_datetime(line);
|
|
|
|
// Parse the optional cleared flag: *
|
|
|
|
transaction_t::state_t state = transaction_t::UNCLEARED;
|
|
if (next) {
|
|
switch (*next) {
|
|
case '*':
|
|
state = transaction_t::CLEARED;
|
|
next = skip_ws(++next);
|
|
break;
|
|
case '!':
|
|
state = transaction_t::PENDING;
|
|
next = skip_ws(++next);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 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>";
|
|
|
|
TRACE_STOP(entry_text, 1);
|
|
|
|
// Parse all of the transactions associated with this entry
|
|
|
|
TRACE_START(entry_details, 1, "Time spent parsing entry details:");
|
|
|
|
unsigned long end_pos;
|
|
unsigned long beg_line = linenum;
|
|
|
|
while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) {
|
|
unsigned long beg_pos = (unsigned long)in.tellg();
|
|
|
|
line[0] = '\0';
|
|
in.getline(line, MAX_LINE);
|
|
if (in.eof() && line[0] == '\0')
|
|
break;
|
|
|
|
int len = std::strlen(line);
|
|
if (line[len - 1] == '\r')
|
|
line[--len] = '\0';
|
|
|
|
end_pos = beg_pos + len + 1;
|
|
linenum++;
|
|
|
|
if (line[0] == ' ' || line[0] == '\t') {
|
|
char * p = skip_ws(line);
|
|
if (! *p)
|
|
break;
|
|
}
|
|
|
|
if (transaction_t * xact = parse_transaction(line, 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;
|
|
pos = end_pos;
|
|
|
|
curr->add_transaction(xact);
|
|
}
|
|
|
|
if (in.eof())
|
|
break;
|
|
}
|
|
|
|
TRACE_STOP(entry_details, 1);
|
|
|
|
return curr.release();
|
|
}
|
|
|
|
static inline void parse_symbol(char *& p, string& symbol)
|
|
{
|
|
if (*p == '"') {
|
|
char * q = std::strchr(p + 1, '"');
|
|
if (! q)
|
|
throw new 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 new 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 new parse_error("Ledger file contains XML data, but format was not recognized");
|
|
#else
|
|
throw new 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(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 new parse_error("Timelog check-out event without a check-in");
|
|
}
|
|
else if (! account) {
|
|
throw new 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 new 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 new parse_error
|
|
("Timelog check-out date less than corresponding check-in");
|
|
|
|
char buf[32];
|
|
std::sprintf(buf, "%lds", long((curr->_date - event.checkin).seconds()));
|
|
amount_t amt;
|
|
amt.parse(buf);
|
|
assert(amt.valid());
|
|
|
|
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 new 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 line[MAX_LINE + 1];
|
|
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() << "'");
|
|
|
|
unsigned long beg_pos = in.tellg();
|
|
unsigned long end_pos;
|
|
unsigned long beg_line = linenum;
|
|
|
|
while (in.good() && ! in.eof()) {
|
|
try {
|
|
in.getline(line, MAX_LINE);
|
|
if (in.eof())
|
|
break;
|
|
|
|
int len = std::strlen(line);
|
|
if (line[len - 1] == '\r')
|
|
line[--len] = '\0';
|
|
|
|
end_pos = beg_pos + len + 1;
|
|
linenum++;
|
|
|
|
switch (line[0]) {
|
|
case '\0':
|
|
break;
|
|
|
|
case ' ':
|
|
case '\t': {
|
|
char * p = skip_ws(line);
|
|
if (*p)
|
|
throw new 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())
|
|
for (std::list<time_entry_t>::iterator i = time_entries.begin();
|
|
i != time_entries.end();
|
|
i++)
|
|
if (event.account == (*i).account)
|
|
throw new 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 new 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_STYLE_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';
|
|
}
|
|
#if 0
|
|
process_option(config_options, line + 2, p);
|
|
#endif
|
|
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", 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 new parse_error(string("Parsing time period '") + line + "'");
|
|
|
|
if (parse_transactions(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 new 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<unsigned long> save_beg_pos(beg_pos);
|
|
push_variable<unsigned long> 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 transaction
|
|
// 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") {
|
|
#if 0
|
|
if (! expr::global_scope.get())
|
|
init_value_expr();
|
|
parse_value_definition(p);
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
unsigned long pos = beg_pos;
|
|
TRACE_START(entries, 1, "Time spent handling entries:");
|
|
if (entry_t * entry =
|
|
parse_entry(in, line, account_stack.front(), *this, pos)) {
|
|
if (journal.add_entry(entry)) {
|
|
entry->src_idx = src_idx;
|
|
entry->beg_pos = beg_pos;
|
|
entry->beg_line = beg_line;
|
|
entry->end_pos = pos;
|
|
entry->end_line = linenum;
|
|
count++;
|
|
} else {
|
|
checked_delete(entry);
|
|
throw new parse_error("Entry does not balance");
|
|
}
|
|
} else {
|
|
throw new parse_error("Failed to parse entry");
|
|
}
|
|
end_pos = pos;
|
|
TRACE_STOP(entries, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (error * err) {
|
|
for (std::list<std::pair<path, int> >::reverse_iterator i =
|
|
include_stack.rbegin();
|
|
i != include_stack.rend();
|
|
i++)
|
|
err->context.push_back(new include_context((*i).first, (*i).second,
|
|
"In file included from"));
|
|
err->context.push_front(new file_context(pathname, linenum - 1));
|
|
|
|
std::cout.flush();
|
|
if (errors > 0 && err->context.size() > 1)
|
|
std::cerr << std::endl;
|
|
err->reveal_context(std::cerr, "Error");
|
|
std::cerr << err->what() << std::endl;
|
|
checked_delete(err);
|
|
errors++;
|
|
}
|
|
beg_pos = end_pos;
|
|
}
|
|
|
|
if (! time_entries.empty()) {
|
|
std::list<account_t *> accounts;
|
|
|
|
for (std::list<time_entry_t>::iterator i = time_entries.begin();
|
|
i != time_entries.end();
|
|
i++)
|
|
accounts.push_back((*i).account);
|
|
|
|
for (std::list<account_t *>::iterator i = accounts.begin();
|
|
i != accounts.end();
|
|
i++)
|
|
clock_out_from_timelog(time_entries, current_moment, *i, NULL, journal);
|
|
|
|
assert(time_entries.empty());
|
|
}
|
|
|
|
if (added_auto_entry_hook)
|
|
journal.remove_entry_finalizer(&auto_entry_finalizer);
|
|
|
|
if (errors > 0)
|
|
throw (int)errors;
|
|
|
|
TRACE_STOP(parsing_total, 1);
|
|
|
|
return count;
|
|
}
|
|
|
|
void write_textual_journal(journal_t& journal, path pathname,
|
|
item_handler<transaction_t>& formatter,
|
|
const string& write_hdr_format,
|
|
std::ostream& out)
|
|
{
|
|
unsigned long index = 0;
|
|
path found;
|
|
|
|
if (pathname.empty()) {
|
|
if (! journal.sources.empty())
|
|
found = *journal.sources.begin();
|
|
} else {
|
|
#ifdef HAVE_REALPATH
|
|
char buf1[PATH_MAX];
|
|
char buf2[PATH_MAX];
|
|
|
|
::realpath(pathname.string().c_str(), buf1);
|
|
|
|
for (paths_list::iterator i = journal.sources.begin();
|
|
i != journal.sources.end();
|
|
i++) {
|
|
::realpath((*i).string().c_str(), buf2);
|
|
if (std::strcmp(buf1, buf2) == 0) {
|
|
found = *i;
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
#else
|
|
for (paths_list::iterator i = journal.sources.begin();
|
|
i != journal.sources.end();
|
|
i++) {
|
|
if (pathname == *i) {
|
|
found = *i;
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (found.empty())
|
|
throw new error(string("Journal does not refer to file '") +
|
|
string(pathname.string()) + "'");
|
|
|
|
entries_list::iterator el = journal.entries.begin();
|
|
auto_entries_list::iterator al = journal.auto_entries.begin();
|
|
period_entries_list::iterator pl = journal.period_entries.begin();
|
|
|
|
unsigned long pos = 0;
|
|
|
|
format_t hdr_fmt(write_hdr_format);
|
|
boost::filesystem::ifstream in(found);
|
|
|
|
while (! in.eof()) {
|
|
entry_base_t * base = NULL;
|
|
if (el != journal.entries.end() && pos == (*el)->beg_pos) {
|
|
hdr_fmt.format(out, details_t(**el));
|
|
base = *el++;
|
|
}
|
|
else if (al != journal.auto_entries.end() && pos == (*al)->beg_pos) {
|
|
out << "= " << (*al)->predicate.predicate.expr_str << '\n';
|
|
base = *al++;
|
|
}
|
|
else if (pl != journal.period_entries.end() && pos == (*pl)->beg_pos) {
|
|
out << "~ " << (*pl)->period_string << '\n';
|
|
base = *pl++;
|
|
}
|
|
|
|
char c;
|
|
if (base) {
|
|
for (transactions_list::iterator x = base->transactions.begin();
|
|
x != base->transactions.end();
|
|
x++)
|
|
if (! (*x)->has_flags(TRANSACTION_AUTO)) {
|
|
transaction_xdata(**x).dflags |= TRANSACTION_TO_DISPLAY;
|
|
formatter(**x);
|
|
}
|
|
formatter.flush();
|
|
|
|
while (pos < base->end_pos) {
|
|
in.get(c);
|
|
pos = in.tellg(); // pos++;
|
|
}
|
|
} else {
|
|
in.get(c);
|
|
pos = in.tellg(); // pos++;
|
|
out.put(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace ledger
|