summaryrefslogtreecommitdiff
path: root/textual.cc
diff options
context:
space:
mode:
authorJohn Wiegley <johnw@newartisans.com>2004-07-26 23:33:51 -0400
committerJohn Wiegley <johnw@newartisans.com>2004-07-26 23:33:51 -0400
commit161d6f79bd6f4ab45afa1cbae77548c8e508809a (patch)
tree55391f4997e20de173579d90b43316a968b27c9e /textual.cc
parentfde56d0f1214b8fb9de5ba4d42d683ed494c45b0 (diff)
downloadfork-ledger-161d6f79bd6f4ab45afa1cbae77548c8e508809a.tar.gz
fork-ledger-161d6f79bd6f4ab45afa1cbae77548c8e508809a.tar.bz2
fork-ledger-161d6f79bd6f4ab45afa1cbae77548c8e508809a.zip
initial rev of 2.0
Diffstat (limited to 'textual.cc')
-rw-r--r--textual.cc902
1 files changed, 902 insertions, 0 deletions
diff --git a/textual.cc b/textual.cc
new file mode 100644
index 00000000..d4938524
--- /dev/null
+++ b/textual.cc
@@ -0,0 +1,902 @@
+#include "ledger.h"
+#include "constraint.h"
+#include "textual.h"
+
+#include <vector>
+#include <fstream>
+#include <sstream>
+#include <cstring>
+#include <ctime>
+#include <cctype>
+
+#define TIMELOG_SUPPORT 1
+
+namespace ledger {
+
+#if 0
+static const std::string entry1_fmt = "%?10d %p";
+static const std::string entryn_fmt = " %-30a %15t";
+#endif
+
+#define MAX_LINE 1024
+
+std::string path;
+unsigned int linenum;
+
+#ifdef TIMELOG_SUPPORT
+static std::time_t time_in;
+static account_t * last_account;
+static std::string last_desc;
+#endif
+
+static std::time_t now = std::time(NULL);
+static struct std::tm * now_tm = std::localtime(&now);
+
+static std::time_t base = -1;
+static int base_year = -1;
+
+static const int month_days[12] = {
+ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+};
+
+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
+};
+
+inline char * skip_ws(char * ptr)
+{
+ while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n')
+ ptr++;
+ return ptr;
+}
+
+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;
+}
+
+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;
+
+ 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 = 0;
+
+ if (when.tm_mday == -1)
+ when.tm_mday = 1;
+
+ *result = std::mktime(&when);
+
+ return true;
+}
+
+static bool quick_parse_date(char * date_str, std::time_t * result)
+{
+ int year = -1, month = -1, day, num = 0;
+
+ for (char * p = date_str; *p; p++) {
+ if (*p == '/' || *p == '-' || *p == '.') {
+ if (year == -1)
+ year = num;
+ else
+ month = num;
+ num = 0;
+ }
+ else if (*p < '0' || *p > '9') {
+ return false;
+ }
+ else {
+ num *= 10;
+ num += *p - '0';
+ }
+ }
+
+ day = num;
+
+ if (month == -1) {
+ month = year;
+ year = -1;
+ }
+
+ if (base == -1 || year != base_year) {
+ struct std::tm when;
+
+ when.tm_hour = 0;
+ when.tm_min = 0;
+ when.tm_sec = 0;
+
+ base_year = year == -1 ? now_tm->tm_year + 1900 : year;
+ when.tm_year = year == -1 ? now_tm->tm_year : year - 1900;
+ when.tm_mon = 0;
+ when.tm_mday = 1;
+
+ base = std::mktime(&when);
+ }
+
+ *result = base;
+
+ --month;
+ while (--month >= 0) {
+ *result += month_days[month] * 24 * 60 * 60;
+ if (month == 1 && year % 4 == 0 && year != 2000) // february in leap years
+ *result += 24 * 60 * 60;
+ }
+
+ if (--day)
+ *result += day * 24 * 60 * 60;
+
+ return true;
+}
+
+inline char peek_next_nonws(std::istream& in)
+{
+ char c = in.peek();
+ while (! in.eof() && std::isspace(c) && c != '\n') {
+ in.get(c);
+ c = in.peek();
+ }
+ return c;
+}
+
+transaction_t * parse_transaction_text(char * line, ledger_t * ledger,
+ account_t * account, entry_t * entry)
+{
+ // The account will be determined later...
+
+ transaction_t * xact = new transaction_t(entry, 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)) {
+ if (char * note_str = std::strchr(cost_str, ';')) {
+ *note_str++ = '\0';
+ xact->note = skip_ws(note_str);
+ }
+
+ char * price_str = std::strchr(cost_str, '@');
+ if (price_str) {
+ *price_str++ = '\0';
+ xact->cost.parse(price_str, ledger);
+ }
+
+ xact->amount.parse(cost_str, ledger);
+
+ if (price_str)
+ xact->cost *= xact->amount;
+ else
+ xact->cost = xact->amount;
+ }
+
+ 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);
+
+ if (! xact->amount.commodity)
+ xact->amount.commodity = ledger->find_commodity("", true);
+ if (! xact->cost.commodity)
+ xact->cost.commodity = ledger->find_commodity("", true);
+
+ return xact;
+}
+
+transaction_t * parse_transaction(std::istream& in, ledger_t * ledger,
+ account_t * account, entry_t * entry)
+{
+ static char line[MAX_LINE + 1];
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ return parse_transaction_text(line, ledger, account, entry);
+}
+
+class automated_transaction_t
+{
+public:
+ masks_list masks;
+ transactions_list transactions;
+
+ automated_transaction_t(masks_list& _masks,
+ transactions_list& _transactions) {
+ masks.insert(masks.begin(), _masks.begin(), _masks.end());
+ transactions.insert(transactions.begin(),
+ _transactions.begin(), _transactions.end());
+ // Take over ownership of the pointers
+ _transactions.clear();
+ }
+
+ ~automated_transaction_t() {
+ for (transactions_list::iterator i = transactions.begin();
+ i != transactions.end();
+ i++)
+ delete *i;
+ }
+
+ void extend_entry(entry_t * entry);
+};
+
+typedef std::vector<automated_transaction_t *>
+ automated_transactions_vector;
+
+void automated_transaction_t::extend_entry(entry_t * entry)
+{
+ for (transactions_list::iterator i = entry->transactions.begin();
+ i != entry->transactions.end();
+ i++)
+ if (matches(masks, *((*i)->account))) {
+ for (transactions_list::iterator t = transactions.begin();
+ t != transactions.end();
+ t++) {
+ amount_t amt;
+ if ((*t)->amount.commodity->symbol.empty())
+ amt = (*i)->amount * (*t)->amount;
+ else
+ amt = (*t)->amount;
+
+ transaction_t * xact
+ = new transaction_t(entry, (*t)->account, amt, amt, (*t)->flags);
+ entry->add_transaction(xact);
+ }
+ }
+}
+
+class automated_transactions_t
+{
+public:
+ automated_transactions_vector automated_transactions;
+
+ ~automated_transactions_t() {
+ for (automated_transactions_vector::iterator i
+ = automated_transactions.begin();
+ i != automated_transactions.end();
+ i++)
+ delete *i;
+ }
+
+ void extend_entry(entry_t * entry) {
+ for (automated_transactions_vector::iterator i
+ = automated_transactions.begin();
+ i != automated_transactions.end();
+ i++)
+ (*i)->extend_entry(entry);
+ }
+
+ void add_automated_transaction(automated_transaction_t * auto_xact) {
+ automated_transactions.push_back(auto_xact);
+ }
+ bool remove_automated_transaction(automated_transaction_t * auto_xact) {
+ for (automated_transactions_vector::iterator i
+ = automated_transactions.begin();
+ i != automated_transactions.end();
+ i++) {
+ if (*i == auto_xact) {
+ automated_transactions.erase(i);
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+void parse_automated_transactions(std::istream& in, ledger_t * ledger,
+ account_t * account,
+ automated_transactions_t& auto_xacts)
+{
+ static char line[MAX_LINE + 1];
+
+ masks_list masks;
+
+ while (! in.eof() && in.peek() == '=') {
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ char * p = line + 1;
+ p = skip_ws(p);
+
+ masks.push_back(mask_t(p));
+ }
+
+ transactions_list xacts;
+
+ while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t')) {
+ if (transaction_t * xact = parse_transaction(in, ledger, account, NULL)) {
+ if (! xact->amount) {
+ std::cerr << "Error in " << path << ", line " << (linenum - 1)
+ << ": All automated transactions must have a value."
+ << std::endl;
+ } else {
+ xacts.push_back(xact);
+ }
+ }
+ }
+
+ if (! masks.empty() && ! xacts.empty()) {
+ automated_transaction_t * auto_xact
+ = new automated_transaction_t(masks, xacts);
+ auto_xacts.add_automated_transaction(auto_xact);
+ }
+}
+
+bool finalize_entry(entry_t * entry)
+{
+ // Scan through and compute the total balance for the entry. This
+ // is used for auto-calculating the value of entries with no cost,
+ // and the per-unit price of unpriced commodities.
+
+ balance_t balance;
+
+ for (transactions_list::const_iterator x = entry->transactions.begin();
+ x != entry->transactions.end();
+ x++)
+ if (! ((*x)->flags & TRANSACTION_VIRTUAL) ||
+ ((*x)->flags & TRANSACTION_BALANCE))
+ balance += (*x)->cost;
+
+ // If one transaction of a two-line transaction is of a different
+ // commodity than the others, and it has no per-unit price,
+ // determine its price by dividing the unit count into the value of
+ // the balance. This is done for the last eligible commodity.
+
+ if (! balance.amounts.empty() && balance.amounts.size() == 2)
+ for (transactions_list::const_iterator x = entry->transactions.begin();
+ x != entry->transactions.end();
+ x++) {
+ if ((*x)->cost != (*x)->amount || ((*x)->flags & TRANSACTION_VIRTUAL))
+ continue;
+
+ for (amounts_map::const_iterator i = balance.amounts.begin();
+ i != balance.amounts.end();
+ i++)
+ if ((*i).second.commodity != (*x)->amount.commodity) {
+ assert((*x)->amount);
+ balance -= (*x)->cost;
+ (*x)->cost = - (*i).second;
+ balance += (*x)->cost;
+ break;
+ }
+
+ break;
+ }
+
+ // Walk through each of the transactions, fixing up any that we
+ // can, and performing any on-the-fly calculations.
+
+ bool empty_allowed = true;
+
+ for (transactions_list::const_iterator x = entry->transactions.begin();
+ x != entry->transactions.end();
+ x++) {
+ if ((*x)->amount || ((*x)->flags & TRANSACTION_VIRTUAL))
+ continue;
+
+ if (! empty_allowed || balance.amounts.empty() ||
+ balance.amounts.size() != 1)
+ return false;
+
+ empty_allowed = false;
+
+ // If one transaction gives no value at all -- and all the
+ // rest are of the same commodity -- then its value is the
+ // inverse of the computed value of the others.
+
+ amounts_map::const_iterator i = balance.amounts.begin();
+ (*x)->amount = (*x)->cost = - balance.amount((*i).first);
+
+ balance = 0;
+ }
+
+ return ! balance;
+}
+
+entry_t * parse_entry(std::istream& in, ledger_t * ledger,
+ account_t * master)
+{
+ entry_t * curr = new entry_t;
+
+ static char line[MAX_LINE + 1];
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ // Parse the date
+
+ char * next = next_element(line);
+
+ if (! quick_parse_date(line, &curr->date)) {
+ std::cerr << "Error in " << path << ", line " << (linenum - 1)
+ << ": Failed to parse date: " << line << std::endl;
+ return NULL;
+ }
+
+ // Parse the optional cleared flag: *
+
+ if (*next == '*') {
+ curr->state = entry_t::CLEARED;
+ 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->payee = next;
+
+ // Parse all of the transactions associated with this entry
+
+ while (! in.eof() && (in.peek() == ' ' || in.peek() == '\t'))
+ if (transaction_t * xact = parse_transaction(in, ledger, master, curr))
+ curr->add_transaction(xact);
+
+ // If there were no transactions, throw away the entry
+
+ if (curr->transactions.empty() || ! finalize_entry(curr)) {
+ delete curr;
+ return NULL;
+ }
+
+ return curr;
+}
+
+//////////////////////////////////////////////////////////////////////
+//
+// Textual ledger parser
+//
+
+unsigned int parse_textual_ledger(std::istream& in, ledger_t *& ledger,
+ account_t * master)
+{
+ static char line[MAX_LINE + 1];
+ char c;
+ int count = 0;
+ commodity_t * time_commodity = NULL;
+
+ std::list<account_t *> account_stack;
+ automated_transactions_t auto_xacts;
+
+ if (! ledger)
+ ledger = new ledger_t;
+
+ if (! master)
+ master = ledger->master;
+
+ account_stack.push_front(master);
+
+ path = ledger->sources.back();
+ linenum = 1;
+
+ while (! in.eof()) {
+ switch (in.peek()) {
+ case -1: // end of file
+ goto done;
+
+ case ' ':
+ case '\t':
+ if (peek_next_nonws(in) != '\n') {
+ std::cerr << "Error in " << path << ", line " << (linenum - 1)
+ << ": Ignoring entry beginning with whitespace."
+ << std::endl;
+ in.getline(line, MAX_LINE);
+ linenum++;
+ break;
+ }
+ // fall through...
+
+ 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 = account_stack.front()->find_account(p);
+ } else {
+ std::cerr << "Error in " << path << ", 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;
+
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ date += " ";
+ date += time;
+
+ static struct std::tm when;
+ if (strptime(date.c_str(), "%Y/%m/%d %H:%M:%S", &when)) {
+ 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) / 60.0 / 60.0;
+ char buf[32];
+ std::sprintf(buf, "%fh", diff);
+ amount_t amt;
+ amt.parse(buf, ledger);
+ time_commodity = amt.commodity;
+
+ transaction_t * xact = new transaction_t(curr, last_account, amt, amt,
+ TRANSACTION_VIRTUAL);
+ curr->add_transaction(xact);
+
+ if (! finalize_entry(curr) || ! ledger->add_entry(curr))
+ assert(0);
+
+ count++;
+ } else {
+ std::cerr << "Error in " << path << ", line " << (linenum - 1)
+ << ": Cannot parse timelog entry date." << std::endl;
+ }
+
+ last_account = NULL;
+ } else {
+ in.getline(line, MAX_LINE);
+ linenum++;
+ }
+ break;
+#endif // TIMELOG_SUPPORT
+
+ case 'P': { // a pricing entry
+ in >> c;
+
+ std::time_t date;
+ std::string symbol;
+
+ in >> line; // the date
+ if (! quick_parse_date(line, &date)) {
+ std::cerr << "Error in " << path << ", line " << (linenum - 1)
+ << ": 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);
+
+ amount_t price;
+
+ parse_commodity(in, symbol);
+ in >> line; // the price
+ price.parse(line, ledger);
+
+ commodity_t * commodity = ledger->find_commodity(symbol, true);
+ commodity->add_price(date, price);
+ break;
+ }
+
+ case 'N': { // don't download prices
+ std::string symbol;
+
+ in >> c;
+ parse_commodity(in, symbol);
+
+ commodity_t * commodity = ledger->find_commodity(line, true);
+ commodity->flags |= (COMMODITY_STYLE_CONSULTED |
+ COMMODITY_STYLE_NOMARKET);
+ break;
+ }
+
+ case 'C': { // a flat conversion
+ in >> c;
+
+ std::string symbol;
+ amount_t price;
+
+ parse_commodity(in, symbol);
+ in >> line; // the price
+ price.parse(line, ledger);
+
+ commodity_t * commodity = ledger->find_commodity(symbol, true);
+ commodity->set_conversion(price);
+ break;
+ }
+
+ case 'Y': // set the current year
+ in >> c;
+ in >> now_tm->tm_year;
+ now_tm->tm_year -= 1900;
+ break;
+
+#ifdef TIMELOG_SUPPORT
+ case 'h':
+ case 'b':
+#endif
+ case ';': // a comment line
+ in.getline(line, MAX_LINE);
+ linenum++;
+ break;
+
+ case '=': // automated transactions
+ parse_automated_transactions(in, ledger, account_stack.front(),
+ auto_xacts);
+ break;
+
+ case '@': { // account specific
+ in >> c;
+ if (in.peek() == '@') {
+ in.get(c);
+ account_stack.pop_front();
+ break;
+ }
+
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ account_t * acct = account_stack.front()->find_account(skip_ws(line));
+ account_stack.push_front(acct);
+ break;
+ }
+
+ case '!': // directive
+ in >> line;
+ if (std::string(line) == "!include") {
+ in.getline(line, MAX_LINE);
+ linenum++;
+
+ char * path = skip_ws(line);
+ std::ifstream stream(path);
+
+ ledger->sources.push_back(path);
+
+ unsigned int curr_linenum = linenum;
+ count += parse_textual_ledger(stream, ledger, account_stack.front());
+ linenum = curr_linenum;
+ }
+ break;
+
+ default: {
+ unsigned int first_line = linenum;
+ if (entry_t * entry = parse_entry(in, ledger, account_stack.front())) {
+ if (! auto_xacts.automated_transactions.empty())
+ auto_xacts.extend_entry(entry);
+
+ if (ledger->add_entry(entry))
+ count++;
+ else
+ std::cerr << "Error in " << path << ", line " << first_line
+ << ": Entry does not balance." << std::endl;
+ } else {
+ std::cerr << "Error in " << path << ", line " << first_line
+ << ": Failed to parse entry." << std::endl;
+ }
+ break;
+ }
+ }
+ }
+
+ done:
+ if (time_commodity) {
+ time_commodity->precision = 2;
+ time_commodity->flags |= (COMMODITY_STYLE_CONSULTED |
+ COMMODITY_STYLE_NOMARKET);
+ }
+
+ return count;
+}
+
+//////////////////////////////////////////////////////////////////////
+//
+// Textual ledger printing code
+//
+
+void print_transaction(std::ostream& out, transaction_t * xact,
+ bool display_amount, bool display_cost)
+{
+ std::ostringstream s;
+ s << *(xact->account);
+ std::string acct_name = s.str();
+
+ if (xact->flags & TRANSACTION_VIRTUAL) {
+ if (xact->flags & TRANSACTION_BALANCE)
+ acct_name = std::string("[") + acct_name + "]";
+ else
+ acct_name = std::string("(") + acct_name + ")";
+ }
+
+ out.width(30);
+ out.fill(' ');
+ out << std::left << acct_name;
+
+ if (xact->amount && display_amount) {
+ out << " ";
+ out.width(12);
+ out.fill(' ');
+ std::ostringstream s;
+ s << xact->amount;
+ out << std::right << s.str();
+ }
+
+ if (xact->amount && display_cost &&
+ xact->amount != xact->cost) {
+ out << " @ ";
+ out << xact->cost / xact->amount;
+ }
+
+ if (! xact->note.empty())
+ out << " ; " << xact->note;
+
+ out << std::endl;
+}
+
+void print_textual_entry(std::ostream& out, entry_t * entry, bool shortcut)
+{
+ char buf[32];
+ std::strftime(buf, 31, "%Y/%m/%d ", std::gmtime(&entry->date));
+ out << buf;
+
+ if (entry->state == entry_t::CLEARED)
+ out << "* ";
+ if (! entry->code.empty())
+ out << '(' << entry->code << ") ";
+ if (! entry->payee.empty())
+ out << entry->payee;
+
+ out << std::endl;
+
+ const commodity_t * comm = NULL;
+ int size = 0;
+
+ for (transactions_list::const_iterator x
+ = entry->transactions.begin();
+ x != entry->transactions.end();
+ x++) {
+ if ((*x)->flags & TRANSACTION_VIRTUAL &&
+ ! ((*x)->flags & TRANSACTION_BALANCE))
+ continue;
+
+ if (! comm)
+ comm = (*x)->amount.commodity;
+ else if (comm != (*x)->amount.commodity)
+ shortcut = false;
+
+ size++;
+ }
+
+ if (shortcut && size != 2)
+ shortcut = false;
+
+ for (transactions_list::const_iterator x
+ = entry->transactions.begin();
+ x != entry->transactions.end();
+ x++) {
+ out << " ";
+ print_transaction(out, *x,
+ (! shortcut || x == entry->transactions.begin() ||
+ ((*x)->flags & TRANSACTION_VIRTUAL &&
+ ! ((*x)->flags & TRANSACTION_BALANCE))),
+ size != 2);
+ }
+
+ out << std::endl;
+}
+
+void print_textual_ledger(std::ostream& out, ledger_t * ledger,
+ bool shortcut)
+{
+ for (entries_list::const_iterator i = ledger->entries.begin();
+ i != ledger->entries.end();
+ i++)
+ print_textual_entry(out, *i, shortcut);
+}
+
+} // namespace ledger
+
+#ifdef PARSE_TEST
+
+int main(int argc, char *argv[])
+{
+ book.sources.push_back(argv[1]);
+ std::ifstream stream(argv[1]);
+ ledger::ledger_t book;
+ int count = parse_textual_ledger(stream, &book, book.master);
+ std::cout << "Read " << count << " entries." << std::endl;
+ print_textual_ledger(std::cout, &book, true);
+}
+
+#endif // PARSE_TEST