diff options
-rw-r--r-- | amount.cc | 7 | ||||
-rw-r--r-- | balance.cc | 196 | ||||
-rw-r--r-- | gnucash.cc | 41 | ||||
-rw-r--r-- | ledger.cc | 10 | ||||
-rw-r--r-- | ledger.h | 46 | ||||
-rw-r--r-- | ledger.texi | 186 | ||||
-rw-r--r-- | main.cc | 65 | ||||
-rw-r--r-- | parse.cc | 21 |
8 files changed, 392 insertions, 180 deletions
@@ -1,5 +1,4 @@ #include <sstream> -#include <cassert> #include <gmp.h> // GNU multi-precision library #include <pcre.h> // Perl regular expression library @@ -337,9 +336,6 @@ amount& gmp_amount::operator=(const char * num) commodities_iterator item = commodities.find(symbol.c_str()); if (item == commodities.end()) { quantity_comm = new commodity(symbol, prefix, separate, precision); - std::pair<commodities_iterator, bool> insert_result = - commodities.insert(commodities_entry(symbol, quantity_comm)); - assert(insert_result.second); } else { quantity_comm = (*item).second; @@ -396,9 +392,6 @@ amount& gmp_amount::operator=(const char * num) commodities_iterator item = commodities.find(symbol.c_str()); if (item == commodities.end()) { price_comm = new commodity(symbol, prefix, separate, precision); - std::pair<commodities_iterator, bool> insert_result = - commodities.insert(commodities_entry(symbol, price_comm)); - assert(insert_result.second); } else { price_comm = (*item).second; @@ -1,10 +1,7 @@ -#include <iostream> -#include <vector> +#include "ledger.h" #include <pcre.h> // Perl regular expression library -#include "ledger.h" - namespace ledger { ////////////////////////////////////////////////////////////////////// @@ -12,10 +9,38 @@ namespace ledger { // Balance report. // -void report_balances(std::ostream& out, std::vector<entry *>& ledger, - bool show_children, bool show_empty) +static inline bool matches(const std::list<pcre *>& regexps, + const std::string& str) { + for (std::list<pcre *>::const_iterator r = regexps.begin(); + r != regexps.end(); + r++) { + int ovec[3]; + if (pcre_exec(*r, NULL, str.c_str(), str.length(), 0, 0, ovec, 3) >= 0) + return true; + } + + return false; +} + +void report_balances(int argc, char *argv[], std::ostream& out) { -#if 0 + bool show_current = false; + bool show_cleared = false; + bool show_children = false; + bool show_empty = false; + bool no_subtotals = false; + + int c; + while (-1 != (c = getopt(argc, argv, "cCsSn"))) { + switch (char(c)) { + case 'c': show_current = true; break; + case 'C': show_cleared = true; break; + case 's': show_children = true; break; + case 'S': show_empty = true; break; + case 'n': no_subtotals = true; break; + } + } + // Compile the list of specified regular expressions, which can be // specified on the command line, or using an include/exclude file. @@ -26,97 +51,118 @@ void report_balances(std::ostream& out, std::vector<entry *>& ledger, int erroffset; pcre * re = pcre_compile(argv[optind], PCRE_CASELESS, &error, &erroffset, NULL); - assert(re); - regexps.push_back(re); + if (! re) + std::cerr << "Warning: Failed to compile regexp: " << argv[optind] + << std::endl; + else + regexps.push_back(re); } -#endif - // The balance of all accounts must equal zero - totals future_balance; - totals current_balance; - totals cleared_balance; + // Walk through all of the ledger entries, computing the account + // totals + + std::map<account *, totals *> balances; + + std::time_t now = std::time(NULL); + + for (ledger_iterator i = ledger.begin(); i != ledger.end(); i++) { + for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); + x != (*i)->xacts.end(); + x++) { + account * acct = (*x)->acct; + if (! regexps.empty() && ! matches(regexps, acct->name)) + continue; + + while (acct) { + totals * balance = NULL; + + std::map<account *, totals *>::iterator t = balances.find(acct); + if (t == balances.end()) { + balance = new totals; + balances.insert(std::pair<account *, totals *>(acct, balance)); + } else { + balance = (*t).second; + } + + if (show_current) { + if (difftime((*i)->date, now) < 0) + balance->credit((*x)->cost); + } + else if (show_cleared) { + if ((*i)->cleared) + balance->credit((*x)->cost); + } + else { + balance->credit((*x)->cost); + } + + if (no_subtotals) + acct = NULL; + else + acct = acct->parent; + } + } + } + + // Print out the balance report header + + std::string which = "Future"; + if (show_current) + which = "Current"; + else if (show_cleared) + which = "Cleared"; std::cout.width(10); - std::cout << std::right << "Future" << " "; - std::cout.width(10); - std::cout << std::right << "Current" << " "; - std::cout.width(10); - std::cout << std::right << "Cleared" << std::endl; + std::cout << std::right << which << std::endl; - for (accounts_iterator i = accounts.begin(); - i != accounts.end(); - i++) { - if (! show_empty && ! (*i).second->future) + // Walk through the accounts, given the balance report for each + + totals total_balance; + + for (accounts_iterator i = accounts.begin(); i != accounts.end(); i++) { + account * acct = (*i).second; + + if (! regexps.empty() && ! matches(regexps, acct->name)) continue; int depth = 0; - account * acct = (*i).second; - while (acct->parent) { + for (account * a = acct; a; a = a->parent) depth++; - acct = acct->parent; - } -#if 0 - if (! regexps.empty()) { - bool matches = false; - for (std::list<pcre *>::iterator r = regexps.begin(); - r != regexps.end(); - r++) { - int ovector[30]; - if (pcre_exec(*r, NULL, (*i).first.c_str(), (*i).first.length(), - 0, 0, ovector, 30) >= 0) { - matches = true; - break; - } - } + if (! show_children && depth) + continue; - if (! matches) - continue; - } - else -#endif - if (! show_children && depth) { + totals * balance = balances[acct]; + + if (! show_empty && ! *balance) continue; - } std::cout.width(10); - std::cout << (*i).second->future << " "; - std::cout.width(10); - std::cout << (*i).second->current << " "; - std::cout.width(10); - std::cout << (*i).second->cleared << " "; + std::cout << *balance << " "; + + total_balance.credit(*balance); if (depth) { while (--depth >= 0) std::cout << " "; - std::cout << (*i).second->name << std::endl; + std::cout << acct->name << std::endl; } else { - std::cout << (*i).first << std::endl; - -#if 0 - if (regexps.empty()) { -#endif - future_balance.credit((*i).second->future); - current_balance.credit((*i).second->current); - cleared_balance.credit((*i).second->cleared); -#if 0 - } -#endif + std::cout << *acct << std::endl; } } -#if 0 - if (regexps.empty()) { -#endif - std::cout.width(10); - std::cout << std::right << future_balance << " "; - std::cout.width(10); - std::cout << std::right << current_balance << " "; - std::cout.width(10); - std::cout << std::right << cleared_balance << std::endl; -#if 0 + // Print the total of all the balances shown + + std::cout.width(10); + std::cout << std::right << total_balance << std::endl; + + // Free up temporary variables created on the heap + + for (std::map<account *, totals *>::iterator i = balances.begin(); + i != balances.end(); + i++) { + delete (*i).second; } -#endif } } // namespace ledger @@ -1,14 +1,12 @@ #include <sstream> -#include <vector> #include <cstring> -#include <cassert> + +#include "ledger.h" extern "C" { #include <xmlparse.h> // expat XML parser } -#include "ledger.h" - namespace ledger { static account * curr_account; @@ -20,8 +18,6 @@ static amount * curr_value; static std::string curr_quant; static XML_Parser current_parser; -static std::vector<entry *> * current_ledger; - enum { NO_ACTION, ACCOUNT_NAME, @@ -116,7 +112,7 @@ static void endElement(void *userData, const char *name) << XML_GetCurrentLineNumber(current_parser) << std::endl; curr_entry->print(std::cerr); } else { - current_ledger->push_back(curr_entry); + ledger.push_back(curr_entry); } curr_entry = NULL; } @@ -192,11 +188,18 @@ static void dataHandler(void *userData, const char *s, int len) case XACT_ACCOUNT: { accounts_iterator i = accounts.find(std::string(s, len)); - assert(i != accounts.end()); - curr_entry->xacts.back()->acct = (*i).second; + if (i == accounts.end()) { + std::cerr << "Could not find account " << std::string(s, len) + << " at line " << XML_GetCurrentLineNumber(current_parser) + << std::endl; + std::exit(1); + } + + transaction * xact = curr_entry->xacts.back(); + xact->acct = (*i).second; std::string value = curr_quant + " " + (*i).second->comm->symbol; - curr_entry->xacts.back()->cost = create_amount(value.c_str(), curr_value); + xact->cost = create_amount(value.c_str(), curr_value); break; } @@ -215,27 +218,25 @@ static void dataHandler(void *userData, const char *s, int len) } } -bool parse_gnucash(std::istream& in, std::vector<entry *>& ledger) +bool parse_gnucash(std::istream& in) { char buf[BUFSIZ]; - XML_Parser parser = XML_ParserCreate(NULL); - current_parser = parser; - - //XML_SetUserData(parser, &depth); - XML_SetElementHandler(parser, startElement, endElement); - XML_SetCharacterDataHandler(parser, dataHandler); - - current_ledger = &ledger; - curr_account = NULL; curr_entry = NULL; curr_comm = NULL; action = NO_ACTION; + XML_Parser parser = XML_ParserCreate(NULL); + current_parser = parser; + + XML_SetElementHandler(parser, startElement, endElement); + XML_SetCharacterDataHandler(parser, dataHandler); + while (! in.eof()) { in.getline(buf, BUFSIZ - 1); + if (! XML_Parse(parser, buf, std::strlen(buf), in.eof())) { std::cerr << XML_ErrorString(XML_GetErrorCode(parser)) << " at line " << XML_GetCurrentLineNumber(parser) @@ -1,13 +1,11 @@ -#include <vector> - #include "ledger.h" namespace ledger { commodities_t commodities; commodity * commodity_usd; - -accounts_t accounts; +accounts_t accounts; +ledger_t ledger; void entry::print(std::ostream& out) const { @@ -105,10 +103,10 @@ amount * totals::value(const std::string& commodity) return total; } -// Print out the entire ledger that was read in, but now sorted. +// Print out the entire ledger that was read in, sorted by date. // This can be used to "wash" ugly ledger files. -void print_ledger(std::ostream& out, std::vector<entry *>& ledger) +void print_ledger(int argc, char *argv[], std::ostream& out) { // Sort the list of entries by date, then print them in order. @@ -1,5 +1,5 @@ #ifndef _LEDGER_H -#define _LEDGER_H "$Revision: 1.2 $" +#define _LEDGER_H "$Revision: 1.3 $" ////////////////////////////////////////////////////////////////////// // @@ -11,9 +11,11 @@ #include <iostream> #include <string> +#include <vector> #include <list> #include <map> #include <ctime> +#include <cassert> namespace ledger { @@ -47,7 +49,7 @@ namespace ledger { // commodity is converted into the first by computing which the price // must have been in order to balance the transaction. Example: // -// 2004.06.18 c (BUY) Apple Computer +// 2004.06.18 * (BUY) Apple Computer // Assets:Brokerage $-200.00 // Assets:Brokerage 100 AAPL // @@ -64,7 +66,7 @@ namespace ledger { // stock, and it will read this transaction as if it had been // written: // -// 2004.06.18 c (BUY) Apple Computer +// 2004.06.18 * (BUY) Apple Computer // Assets:Brokerage $-200 // Assets:Brokerage 100 AAPL @ $2 // @@ -72,7 +74,7 @@ namespace ledger { // exchange for services rendered, use the regular single-commodity // form of transaction: // -// 2004.07.11 c A kick-back for the broker +// 2004.07.11 * A kick-back for the broker // Assets:Brokerage -10 AAPL // Expenses:Broker's Fees 10 AAPL // @@ -92,8 +94,7 @@ struct commodity int precision; commodity() : prefix(false), separate(true) {} - commodity(const std::string& sym, bool pre, bool sep, int prec) - : symbol(sym), prefix(pre), separate(sep), precision(prec) {} + commodity(const std::string& sym, bool pre, bool sep, int prec); }; typedef std::map<const std::string, commodity *> commodities_t; @@ -103,6 +104,15 @@ typedef std::pair<const std::string, commodity *> commodities_entry; extern commodities_t commodities; extern commodity * commodity_usd; +inline commodity::commodity(const std::string& sym, + bool pre, bool sep, int prec) + : symbol(sym), prefix(pre), separate(sep), precision(prec) { + std::pair<commodities_iterator, bool> result = + commodities.insert(commodities_entry(sym, this)); + assert(result.second); +} + + class amount { public: @@ -181,6 +191,12 @@ struct cmp_entry_date { } }; +typedef std::vector<entry *> ledger_t; +typedef ledger_t::iterator ledger_iterator; + +extern ledger_t ledger; + + class totals { typedef std::map<const std::string, amount *> map_t; @@ -217,6 +233,7 @@ operator<<(std::basic_ostream<char, Traits>& out, const totals& t) { return out; } + struct account { std::string name; @@ -231,26 +248,9 @@ struct account map children; - // Balance totals, by commodity - totals future; - totals current; - totals cleared; - account(const std::string& _name, struct account * _parent = NULL) : name(_name), parent(_parent) {} - void credit(const entry * ent, const amount * amt) { - for (account * acct = this; acct; acct = acct->parent) { - acct->future.credit(amt); - - if (difftime(ent->date, std::time(NULL)) < 0) - acct->current.credit(amt); - - if (ent->cleared) - acct->cleared.credit(amt); - } - } - operator std::string() const { if (! parent) return name; diff --git a/ledger.texi b/ledger.texi new file mode 100644 index 00000000..212ca041 --- /dev/null +++ b/ledger.texi @@ -0,0 +1,186 @@ +\input texinfo @c -*-texinfo-*- +@comment $Id: ledger.texi,v 1.1 2003/09/30 00:09:43 johnw Exp $ +@comment %**start of header + +@setfilename ledger.info + +@settitle Ledger Accouting Tool +@syncodeindex pg cp +@comment %**end of header + +@dircategory Ledger Accouting Tool +@direntry +* ledger: (ledger)The Ledger Accouting Tool. +@end direntry + +@titlepage +@title Ledger Accouting Tool +@author John Wiegley <@email{johnw@@newartisans.com}> +@page +@vskip 0pt plus 1filll +@c @insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top +@top Ledger Accouting Tool + +@c @insertcopying +@end ifnottex + +@chapter Introduction + +@code{ledger} is an accouting tool that has the chutzpah to exist. It +provides not one bell or whistle for the money, and returns the user +back to the days before user interfaces were even a twinkle on their +father's CRT. + +What it does do is provide a double-entry accouting ledger with all of +the flexibility and muscle of its modern day cousins---without any of +the fat. Think of it as the bran muffin of accouting tools. + +To begin with, you need to start keeping a ledger. This is the basis +of all accouting, and if you don't know how to do it, now is the time +to learn. The little booklet that comes with your checkbook is a +ledger, so we'll describe double-entry accouting in terms of that. + +A checkbook ledger records debits (subtractions, or withdrawals) and +credits (additions, or deposits) with reference to a single account: +your checking account. Where the money comes from, and where it goes +to, are simply described in the memo field where you write the person +or the company's name. The ultimate aim of keeping a checkbook ledger +is so you know how much money is available to spend at all times. +That is really the aim of all ledgers. + +What computers add is the ability to walk through all of those +transactions and tell you things about your spending habits; let you +devise budgets to get control over your spending; squirrel away money +into virtual savings account without having to physically move the +money around; etc. As you keep your checkbook ledger, you are +recording a lot of information about your life and your habits, and +sometimes that information can tell you things you aren't even aware +of. That is the aim of all good accouting tools. + +The next step up from a checkbook ledger is a ledger that covers all +of your accounts, not just your checking account. In this ledger, you +write not only who the money goes to---in the case of a debit---but +where the money is coming from. In the checkbook ledger, its assumed +that all of the money is coming from your checking account. But in a +general ledger, you have to write two-lines: The source and target. +There must always be a debit from some account for any credit made to +anyone else. This is what is meant by ``double-entry'' accouting. + +For example, let's say you have a checking account and a brokerage +account, and that you can write checks from both of them. Rather than +keeping two checkbooks, you decide to use one ledger for both. Once +you get the hang of this, you'll be ready to use one ledger for all of +your accouting needs, which gets you to the point of this +introduction. + +So in your general ledger, you need to pay Pacific Bell Telephone for +your monthly phone bill. The cost is $23.00. In a checkbook ledger, +you would write out a line that credits your account with Pacific Bell +by $23 as follows: + +@example +9/29 100 Pacific Bell $23.00 $77.00 +@end example + +Very simple: You've written check #100 for $23 to Pacific Bell, which +leaves your balance in checking at $77. + +But in a general ledger, you need to say where the money is coming +from. A general ledger entry would look like this: + +@example +9/29 100 Pacific Bell $23.00 $223.00 + Checking $-23.00 $77.00 +@end example + +What does all of this mean? The first line shows a credit (or +payment) to Pacific Bell to the tune of $23.00. Then, because there +is no one ``balance'' in a general ledger, we've written in the total +balance of your payments to the account ``Pacific Bell''. This was +done by looking at the last entry for ``Pacific Bell'' in the general +ledger, adding $23.00 to that amount, and writing in the total in the +balance column. + +Secondly, the money is coming from your ``Checking'' account, which +means a debit (or withdrawal) of $23.00, which will leave the ending +balance in your ``Checking'' account at $77.00. + +The transaction itself must balance to $0: $23 goes to Pacific Bell, +$23 comes from Checking: there is nothing left over to be accounted +for. The money has in fact moved from one account to another. This +is basis of double-entry accounting: That money never pops out of +existence, it is always described as a transaction between +accounts---as a flow from one place to another. + +Keeping a general ledger is the same as keeping two separate ledgers: +One for Pacific Bell and one for Checking. In that case, each time +you write a credit into one, you write a corresponding debit into the +other. This makes it much easier to write in the running balance, +since you don't have to go looking back for the last time an account +was referenced, but it also means having a lot of ledger books if you +deal with multiple accounts. + +Enter the beauty of a computerized accouting tool. The purpose of +@code{ledger} is to make general ledger accouting simple by keeping +track of the balances for you. Your only job is to enter credit/debit +pairs and make sure they balance. If a transaction does not balance, +@code{ledger} will display an error and ignore the +transaction.@footnote{In some special cases, it will automatically +balance the entry for you.} + +Your usage of @code{ledger} will have two parts: Keeping the ledger, +and using the @code{ledger} tool to provide you with information +summaries derived from your ledger's entries. + +@chapter Keeping a ledger + +The most important part of accounting is keeping a good ledger. If +you have a good ledger, tools can be written to work whatever +mathematically tricks you need to better understand your spending +patterns. Without a good ledger, no tool, however smart, can help +you. + +The @code{ledger} program aims at making ledger entry as simple as +possible. Since it is a command-line tool, it does not provide a user +interface for keeping a ledger. If you like, you may use +@code{gnucash} to maintain your ledger, in which case the +@code{ledger} program will read @code{gnucash}'s data files directly. +In that case, read the @code{gnucash} manual now, and skip to the next +chapter. + +If you are not using @code{gnucash}, but a text editor to maintain +your ledger, read on. @code{ledger} has been designed to make data +entry as simple as possible, by keeping the ledger format easy, and +also by automagically determining as much information as possible +based on the nature of your entries. + +For example, you do not need to tell @code{ledger} about the accounts +you use. Any time @code{ledger} sees a debit or a credit to an +account it knows nothing about, it will create it. If you use a +commodity that is new to @code{ledger}, it will create that commodity, +and determine its display characteristics (placement of the symbol +before or after the amount, display precision, etc) based on how you +used the commodity in the transaction. + +Here is the Pacific Bell example from above, given as a @code{ledger} +transaction: + +@example +9/29 (100) Pacific Bell + Expenses:Utilities:Telephone $23.00 + Assets:Checking $-23.00 +@end example + +As you can see, it is very similar to what would be written on paper, +minus the computed balance totals, and adding in account names that +work better with @code{ledger}'s scheme of things. + +@chapter Using @code{ledger} + +@bye @@ -1,44 +1,49 @@ #include <fstream> -#include <vector> -#include <cassert> - -#include <pcre.h> // Perl regular expression library #include "ledger.h" +#include <pcre.h> // Perl regular expression library + ////////////////////////////////////////////////////////////////////// // // Command-line parser and top-level logic. // namespace ledger { - extern bool parse_ledger(std::istream& in, std::vector<entry *>& ledger); - extern bool parse_gnucash(std::istream& in, std::vector<entry *>& ledger); - extern void report_balances(std::ostream& out, std::vector<entry *>& ledger, - bool show_children, bool show_empty); - extern void print_ledger(std::ostream& out, std::vector<entry *>& ledger); + extern bool parse_ledger(std::istream& in); + extern bool parse_gnucash(std::istream& in); + + extern void report_balances(int argc, char *argv[], std::ostream& out); + extern void print_ledger(int argc, char *argv[], std::ostream& out); } using namespace ledger; +void show_help(std::ostream& out) +{ + out << "usage: ledger [options] DATA_FILE COMMAND [ARGS]" + << std::endl + << "options:" << std::endl + << " -s show sub-accounts in balance totals" << std::endl + << " -S show empty accounts in balance totals" << std::endl + << "commands:" << std::endl + << " balance show balance totals" << std::endl + << " print print all ledger entries" << std::endl; +} + int main(int argc, char *argv[]) { - // Setup global defaults + // Global defaults commodity_usd = new commodity("$", true, false, 2); - commodities.insert(commodities_entry("$", commodity_usd)); commodities.insert(commodities_entry("USD", commodity_usd)); // Parse the command-line options - bool show_children = false; - bool show_empty = false; - int c; - while (-1 != (c = getopt(argc, argv, "sS"))) { + while (-1 != (c = getopt(argc, argv, "+h"))) { switch (char(c)) { - case 's': show_children = true; break; - case 'S': show_empty = true; break; + case 'h': show_help(std::cout); break; } } @@ -51,36 +56,28 @@ int main(int argc, char *argv[]) << "commands:" << std::endl << " balance show balance totals" << std::endl << " print print all ledger entries" << std::endl; - std::exit(1); + return 1; } // Parse the ledger std::ifstream file(argv[optind++]); - std::vector<entry *> ledger; - char buf[256]; - file.get(buf, 255); + char buf[32]; + file.get(buf, 31); file.seekg(0); if (std::strncmp(buf, "<?xml version=\"1.0\"?>", 21) == 0) - parse_gnucash(file, ledger); + parse_gnucash(file); else - parse_ledger(file, ledger); - - // Read the command word - - if (optind == argc) { - std::cerr << "Command word missing" << std::endl; - return 1; - } - - const std::string command = argv[optind++]; + parse_ledger(file); // Process the command + const std::string command = argv[optind]; + if (command == "balance") - report_balances(std::cout, ledger, show_children, show_empty); + report_balances(argc - optind, &argv[optind], std::cout); else if (command == "print") - print_ledger(std::cout, ledger); + print_ledger(argc - optind, &argv[optind], std::cout); } @@ -1,14 +1,11 @@ -#include <iostream> -#include <vector> #include <cstring> #include <ctime> #include <cctype> -#include <cassert> - -#include <pcre.h> // Perl regular expression library #include "ledger.h" +#include <pcre.h> // Perl regular expression library + namespace ledger { ////////////////////////////////////////////////////////////////////// @@ -37,8 +34,7 @@ char * next_element(char * buf, bool variable = false) static int linenum = 0; -void finalize_entry(entry * curr, std::vector<entry *>& ledger) -{ +inline void finalize_entry(entry * curr) { if (curr) { if (! curr->validate()) { std::cerr << "Failed to balance the following transaction, " @@ -50,7 +46,7 @@ void finalize_entry(entry * curr, std::vector<entry *>& ledger) } } -bool parse_ledger(std::istream& in, std::vector<entry *>& ledger) +bool parse_ledger(std::istream& in) { static std::time_t now = std::time(NULL); static struct std::tm * now_tm = std::localtime(&now); @@ -94,7 +90,7 @@ bool parse_ledger(std::istream& in, std::vector<entry *>& ledger) } if (curr) - finalize_entry(curr, ledger); + finalize_entry(curr); curr = new entry; // Parse the date @@ -165,11 +161,6 @@ bool parse_ledger(std::istream& in, std::vector<entry *>& ledger) current = (*i).second; } } - - // Apply transaction to account (and all parent accounts) - - assert(current); - current->credit(curr, xact->cost); } xact->acct = current; @@ -181,7 +172,7 @@ bool parse_ledger(std::istream& in, std::vector<entry *>& ledger) } if (curr) - finalize_entry(curr, ledger); + finalize_entry(curr); return true; } |