diff options
-rw-r--r-- | LICENSE | 29 | ||||
-rw-r--r-- | Makefile | 86 | ||||
-rw-r--r-- | NEWS | 160 | ||||
-rw-r--r-- | README | 293 | ||||
-rw-r--r-- | account.cc | 91 | ||||
-rw-r--r-- | amount.cc | 942 | ||||
-rw-r--r-- | balance.cc | 107 | ||||
-rw-r--r-- | balance.h | 594 | ||||
-rw-r--r-- | binary.cc | 621 | ||||
-rw-r--r-- | binary.h | 21 | ||||
-rw-r--r-- | constraint.cc | 219 | ||||
-rw-r--r-- | constraint.h | 186 | ||||
-rw-r--r-- | expr.cc | 602 | ||||
-rw-r--r-- | expr.h | 112 | ||||
-rw-r--r-- | format.cc | 195 | ||||
-rw-r--r-- | format.h | 51 | ||||
-rw-r--r-- | gnucash.cc | 148 | ||||
-rw-r--r-- | item.cc | 224 | ||||
-rw-r--r-- | item.h | 57 | ||||
-rw-r--r-- | ledger.cc | 686 | ||||
-rw-r--r-- | ledger.el | 295 | ||||
-rw-r--r-- | ledger.h | 595 | ||||
-rw-r--r-- | main.cc | 899 | ||||
-rw-r--r-- | parse.cc | 582 | ||||
-rw-r--r-- | reports.cc | 1225 | ||||
-rw-r--r-- | sample.dat | 11 | ||||
-rw-r--r-- | scripts/README | 5 | ||||
-rwxr-xr-x | scripts/bal | 27 | ||||
-rwxr-xr-x | scripts/entry | 16 | ||||
-rwxr-xr-x | scripts/getquote | 16 | ||||
-rwxr-xr-x | scripts/mean | 27 | ||||
-rwxr-xr-x | scripts/profit | 2 | ||||
-rwxr-xr-x | scripts/reg | 14 | ||||
-rwxr-xr-x | scripts/report | 17 | ||||
-rwxr-xr-x | scripts/spending | 8 | ||||
-rwxr-xr-x | scripts/worth | 2 | ||||
-rw-r--r-- | textual.cc | 902 | ||||
-rw-r--r-- | textual.h | 24 |
38 files changed, 6427 insertions, 3664 deletions
diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d0050a15 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +Copyright (c) 2003-2004, New Artisans LLC. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +- Neither the name of New Artisans LLC nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @@ -1,28 +1,43 @@ -CODE = amount.cc ledger.cc parse.cc reports.cc +CODE = amount.cc balance.cc account.cc ledger.cc \ + constraint.cc item.cc expr.cc format.cc \ + textual.cc binary.cc OBJS = $(patsubst %.cc,%.o,$(CODE)) #CXX = cc CXX = g++ -CFLAGS = #-Wall -ansi -pedantic -DFLAGS = -O3 -fomit-frame-pointer -#DFLAGS = -g -DDEBUG=1 -INCS = -I/usr/local/include -LIBS = -L/usr/local/lib -lgmpxx -lgmp -lpcre +CFLAGS = -Wall -ansi -pedantic +#DFLAGS = -O3 -fomit-frame-pointer +DFLAGS = -g -DDEBUG=1 +#DFLAGS = -g -pg +INCS = -I/sw/include -I/usr/include/gcc/darwin/3.3/c++ -I/usr/include/gcc/darwin/3.3/c++/ppc-darwin +LIBS = -L/sw/lib -lgmpxx -lgmp -lpcre ifdef GNUCASH CODE := $(CODE) gnucash.cc CFLAGS := $(CFLAGS) -DREAD_GNUCASH=1 -INCS := $(INCS) -I/usr/include/xmltok -LIBS := $(LIBS) -lxmlparse +INCS := $(INCS) -I/usr/include/httpd/xml +LIBS := $(LIBS) -L/sw/lib -lxmlparse endif -all: make.deps ledger ledger.info +all: make.deps ledger -install: all - strip ledger +docs: ledger.info ledger.pdf + +install: + make clean + make DFLAGS="-O3 -fomit-frame-pointer" cp ledger $(HOME)/bin + strip $(HOME)/bin/ledger + +libledger.a: $(OBJS) + ar rv $@ $? + ranlib $@ + +ledger: libledger.a main.o + $(CXX) $(CFLAGS) $(INCS) $(DFLAGS) -o $@ main.o -L. -lledger $(LIBS) -ledger: $(OBJS) - $(CXX) $(CFLAGS) $(INCS) $(DFLAGS) -o $@ $(OBJS) $(LIBS) +report: libledger.a report.cc + $(CXX) $(CFLAGS) $(INCS) $(DFLAGS) -DTEST -o $@ report.cc \ + -L. -lledger $(LIBS) ledger.info: ledger.texi makeinfo $< @@ -34,11 +49,12 @@ ledger.pdf: ledger.texi $(CXX) $(CFLAGS) $(INCS) $(DFLAGS) -c -o $@ $< clean: - rm -f ledger *.o *.elc *~ .\#* .gdb_history README.texi + rm -f ledger report libledger.a *.o *.elc *~ .\#* rm -f *.aux *.cp *.fn *.ky *.log *.pg *.toc *.tp *.vr + rm -f .gdb_history distclean fullclean: clean - rm -f ledger.info README.html *.pdf *.elc make.deps TAGS + rm -f *.texi *.info *.html *.pdf *.elc make.deps TAGS rebuild: clean deps all @@ -48,3 +64,43 @@ make.deps: Makefile cc -M $(INCS) $(CODE) > $@ include make.deps + +# These next rules are for my own use. + +README.html: README + (cd $(HOME)/src/muse && \ + ./publish --html $(shell pwd)/README && \ + mv README.html $(shell pwd)) + +ledger.texi: README + (cd $(HOME)/src/muse && \ + ./publish --texi $(shell pwd)/README && \ + cat README.texi | sed 's/README\.info/ledger.info/g' \ + > $(shell pwd)/ledger.texi && \ + rm README.texi) + +VERSION = $(shell scripts/version) + +dist: + rm -fr /tmp/ledger-$(VERSION) + rsync -av --exclude=".*" --exclude="TAGS" --exclude="version" \ + --exclude="_darcs/" --exclude="ledger.dat" \ + $(shell pwd)/ /tmp/ledger-$(VERSION) + (cd /tmp/ledger-$(VERSION) && \ + make fullclean && \ + make docs README.html && \ + make clean && rm make.deps && \ + cat Makefile | sed 's/\/sw\//\/usr\/local\//g' > t && \ + mv t Makefile && \ + cat Makefile | sed 's/ -I\/usr\/include\/gcc.*//' > t && \ + mv t Makefile && \ + perl -ne 'print if 1 .. /^include make.deps/;' Makefile > t && \ + mv t Makefile && \ + cd $(HOME)/Public && \ + tar cvzf ledger-$(VERSION).tar.gz /tmp/ledger-$(VERSION)) + +publish: dist + (cd $(HOME)/Public && \ + ln -sf ledger-$(VERSION).tar.gz ledger.tar.gz && \ + rm -fr /tmp/ledger-$(VERSION) && \ + upload) @@ -1,13 +1,157 @@ -1.7 - Pricing histories are now supported, so that ledger remembers - historical prices of all commodities (if this information is - provided), and can give register reports based on past and present - market values, as well as original cost basis. + Ledger NEWS -1.6 +* 2.0 - Can now parse timeclock files. These are simple timelogs that track +- The code base has been rewritten for clarity and consistency. As a + result, the code is much simpler and more robust (and in most cases + faster). + +- Register reports now show the account being credited/debited. Use + new -o option to see "other accounts" -- or the account the + credit/debit came from. (This was the old behavior in 1.x, but can + lead to confusing reports when viewing accounts with subaccounts.) + The -o option also works for balance reports, where it will show all + the account totals related to your query. + +- Regexps specified after the command are applied to account names + only. To search on a payee name, use "--" to separate the two kinds + of regexps. For example, to find payee's named John within all + Expenses accounts: + + ledger register expenses -- john + +- The use of "+" and "-" in ledger files (to specify permanent + regexps) has been removed. + +- The -G switch no longer generates gnuplot-safe data. It now reports + totals in terms of net gain/loss. + +- To include entries from a file into a specific account, use: + + @ ACCOUNT + !include FILE + @@ + + All entries specified within the "@ ACCOUNT/@@" range will be added + under that account. + +- If the environment variable LEDGER_CACHE is set to a filename, a a + binary dump of the current ledger will be written then, to speed up + later queries of the same data. This only happens if no "-f" flag + was seen (i.e., if the LEDGER environment variable is used). + +- There are several new default reporting styles, which work both in + the balance and register reports: + + -O Show base values (this is the default, and old behavior) + -B Show the basis cost of commodities + -V Show the last known market value of commodities + -G Report net gain/loss (shows commodity changes only) + -A Report average value (arithmetic mean) + -D Report deviation from the average value + -Z Report the trend (average rate of change) + -W Report the trend, with older values affecting the trend less + -X Report expected amount for the next transaction + +- Amount expressions are now supported, where the totals reported can + be changed using -t and -T and an expression string composed of: + + a amount + B current balance + T amount total (B + a) + c cost + C cost total + v market value(amount, date) + V total market value(amount total, date) + g net gain (v - c) + G total net gain (V - C) + d date (in seconds past the epoch) + i index (within the report) + o item age = date - report begin date + w item newness = report end date - date + + b report begin date + e report end date + + P(x,y) market price of x at time y (i.e., V = p(A,d)) + Mx total arthmetic of x (x/n) + Bx deviation = a - x + Ax absolute (positive) value of x + {x} parse x as a ledger amount + /s/ 1 if full account name matches s + + in precedence order: + -?[0-9]*.[0-9]+ numerical constants + binary operators: * / + binary operators: + - + comparison operators: = < <= > >= + logical operators: ! & | + (x) parenthetical grouping + + The standard reports are therefore implemented as: + + -O == -t a -T A + -B == -t c -T C + -V == -t c -T V + -G == -t g -T G + -A == -t a -T MA + -D == -t a -T DMA + -T == -t a -T MDMA + -W == -t a -T MD(MA*(d-b/e-b)) + -X == -t a -T a+MD(MA*(d-b/e-b)) + +- The -l flag now takes an expression string as a "predicate". + Therefore, to equal the old behavior of "-l $100", you would use: + + -l AT<{$100} + +- The -S flag now takes an expression string, which yields the value + that will be sorted on. + +- User-specified format strings are supported with a -F option. The + strings are very much like printf format strings, with the following + syntax for each substitution: + + %?-m.MX + + Each part of the above except for X is optional, and means: + + ? field is blank if reporting on a "subsequent line" (within + the register report, when viewing splits) + - left justify field (default is right) + m minimum width of the field + . indicates that a maximum width is being specified + M maximum width of the field; values are truncated to fit + + And where X may be any one of: + + % self-insert + d same as %[%Y/%m/%d] + [datefmt] pass 'datefmt' to strftime for the entry + p the payee + a the custom account name (report dependent) + n the real account name (balance report only) + N full account name (balance report only) + t whatever the specified value expression is + T whatever the specified total expression is + (expr) insert an arbitrary style expression + + The default format for the register and balance reports are: + + %?10d %?-.20p %-.22a %12.66t %12.80T + %20T %-a + +* 1.7 (never released) + +- Pricing histories are now supported, so that ledger remembers + historical pricing of all commodities, and can give register reports + based on past and present market values, as well as the original + cost basis. See README for more details on the new option switches. + +* 1.6 + +- Can now parse timeclock files. These are simple timelogs that track in/out events, which can be maintained using my timeclock tool. By allowing ledger to parse these, it means that reporting can be done on them in the same way as a ledger file (the commodities is "h", @@ -16,4 +160,4 @@ hours into a dollar value via a receivable account, is now trivial. See the docs for more on how to do this. - Began keeping NEWS file. :) +- Began keeping NEWS file. :) @@ -57,11 +57,11 @@ The entry might look like this: <example> 9/29 BAL Pacific Bell $-200.00 $-200.00 - Equity:Opening Balances $200.00 + Equity:Opening Balances $200.00 9/29 BAL Checking $100.00 $100.00 - Equity:Opening Balances $-100.00 + Equity:Opening Balances $-100.00 9/29 100 Pacific Bell $23.00 $223.00 - Checking $-23.00 $77.00 + Checking $-23.00 $77.00 </example> The first line shows a credit (or payment) to Pacific Bell for $23.00. @@ -264,6 +264,236 @@ spending *from* them (debit) in order pay someone else (credit). They are called credit cards because you are able to instantly credit that other person, by simply waving a card. +** Assets and Liabilities + +Assets are money that you have, and Liabilities are money that you +owe. "Liabilities" is just a more inclusive name for Debts. + +An Asset is typically increased by transferring money from an Income +account, such as when you get paid. Here is a typical entry: + +<example> +2004/09/29 My Employer + Assets:Checking $500.00 + Income:Salary +</example> + +Money, here, is coming from an Income account belonging to "My +Employer", and is being transferred to an account that belongs to you. +The money is now yours, which makes it an asset. + +Liability accounts track money you owe to others. They come into play +whenever you borrow money to buy something, or if you owe someone +money. The usual way a liability is changed is by expending money, +thus transferring it to an Expenses account. For example: + +<example> +2004/09/30 Restaurant + Expenses:Dining $25.00 + Liabilities:MasterCard +</example> + +Your account balance will now show $25 spent on Dining, and a +corresponding $25 owed on your MasterCard. The MasterCard liability +will show up as negative, since it offsets the value of your assets. +*The combined total of your Assets and Liabilities is your net worth*. +To see your current net worth, use this command: + +<example> +$ ledger balance ^assets ^liabilities +</example> + +Relatedly, your Income accounts will show up negative, because they +transfer money *from* an account in order to credit your assets. Your +Expenses accounts will show up positive, because that is where the +money went. The combined total your Income and Expenses is your cash +flow. A negative cash flow means that you are spending more money +than you make. To see your current cash flow, use this command: + +<example> +$ ledger balance ^income ^expenses +</example> + +Often, it is only important to view your income and expenses when +asking questions like, "Where did my money go? Am I spending too much +on X? Am I making enough to cover my expenses?" But most of the +time, you will usually want to ask other questions like, "Is there +enough money in my checking account to cover my next credit card +bill?" For these reasons, I recommend creating a script that removes +Income, Expenses, and Equity by default from your basic balance +report. The provided script "bal" does this for you, as well as +making it easier to run the balance command: + +<example> +$ bal +</example> + +To use this script, it must be copied from the **scripts** directory in +the ledger distribution, to a directory along your =PATH=. Also, you +must set the environment variable =LEDGER= to point to your main +ledger file. + +Another common question to ask of your expenses is: How much do I +spend each month on X? Ledger provides a simple way of displaying +monthly totals for any account. Here is an example that summarizes +monthly automobile expenses: + +<example> +$ ledger -M register expenses:auto +</example> + +This assumes, of course, that you use accounts like Expenses:Auto:Gas +and Expenses:Auto:Repair. + +*** Tracking reimbursable expenses + +Sometimes you will want to spend money on behalf of someone else, +which will eventually get repaid. Since the money is still "yours", +it is really an asset. And since the expenditure was for someone +else, you don't want it contaminating your Expenses reports. You will +need to keep an account for tracking reimbursements. + +This is fairly easy to do in ledger. When spending the money, spend +it *to* your Assets:Reimbursements, using a different account for each +person or business that you spend money for. For example: + +<example> +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard +</example> + +This shows that you spent $100.00 on your MasterCard at Circuit City, +but that the expense was made on behalf of Company XYZ. Later, when +Company XYZ pays you back, you will transfer the money from your +reimbursement account to a regular asset account: + +<example> +2004/09/29 Company XYZ + Assets:Checking $100.00 + Assets:Reimbursements:Company XYZ +</example> + +This deposits the money owed from Company XYZ into your checking +account, presumably because they paid you back with a check. + +But what to do if you run your own business, and you want to keep +track of expenses made on your own behalf, while still tracking +everything in a single ledger file? This is more complex, because you +need to track two separate things: 1) The fact that the money should +be reimbursed to you, and 2) What the expense account was, so that you +can later determine where your company is spending its money. + +This kind of transaction is best handled with mirrored transactions in +two different files, one for your personal accounts, and one for your +company accounts. But keeping them in one file involves the same +kinds of transactions, so those are what is shown here. First, the +personal entry, which shows the need for reimbursement: + +<example> +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard +</example> + +This is the same as above, except that you own Company XYZ, and are +keeping track of its expenses in the same ledger file. This entry +should be immediately followed by an equivalent entry, which shows the +kind of expense, and also notes the fact that $100.00 is now payable +to you: + +<example> +2004/09/29 Circuit City + Company XYZ:Expenses:Computer:Software $100.00 + Company XYZ:Accounts Payable:Your Name +</example> + +This second entry shows that Company XYZ has just spent $100.00 on +software, and that this $100.00 came from Your Name, which must be +paid back. + +These two entries can also be merged, to make things a little clearer. +Note that all amounts must be specified now: + +<example> +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard $-100.00 + Company XYZ:Expenses:Computer:Software $100.00 + Company XYZ:Accounts Payable:Your Name $-100.00 +</example> + +To "pay back" the reimbursement, just reverse the order of everything, +except this time drawing the money from a company asset, paying it to +accounts payable, and then drawing it again from the reimbursement +account, and paying it to your personal asset account. It's easier +shown than said: + +<example> +2004/10/15 Company XYZ + Assets:Checking $100.00 + Assets:Reimbursements:Company XYZ $-100.00 + Company XYZ:Accounts Payable:Your Name $100.00 + Company XYZ:Assets:Checking $-100.00 +</example> + +And now the reimbursements account is paid off, accounts payable is +paid off, and the $100.00 has been effectively transferred from the +company's checking account to your personal checking account. The +money simply "waited" -- in both Assets:Reimbursements:Company XYZ, +and Company XYZ:Accounts Payable:Your Name -- until such time as it +could be paid off. + +The value of tracking expenses from both sides like that is that you +do not contaminate your personal expense report with expenses made on +behalf of others, while at the same time making it possible to +generate accurate reports of your company's expenditures. It is more +verbose than just paying for things with your personal assets, but it +gives you a very accurate information trail. + +The advantage to keep these doubled entries together is that they +always stay in sync. The advantage to keeping them apart is that it +clarifies the transfer's point of view. To keep the transactions in +separate files, just separate the two entries that were joined above. +For example, for both the expense and the pay-back shown above, the +following four entries would be created. Two in your personal ledger +file: + +<example> +2004/09/29 Circuit City + Assets:Reimbursements:Company XYZ $100.00 + Liabilities:MasterCard $-100.00 + +2004/10/15 Company XYZ + Assets:Checking $100.00 + Assets:Reimbursements:Company XYZ $-100.00 +</example> + +And two in your company ledger file: + +<example> +@ Company XYZ + +2004/09/29 Circuit City + Expenses:Computer:Software $100.00 + Accounts Payable:Your Name $-100.00 + +2004/10/15 Company XYZ + Accounts Payable:Your Name $100.00 + Assets:Checking $-100.00 + +@@ +</example> + +(Note: The @ above command means that all accounts mentioned in the +file are children of the specified account. In this case it means +that all activity in file relates to Company XYZ). + +After creating these entries, you will always know that $100.00 was +spent using your MasterCard on behalf of Company XYZ, and that Company +XYZ spent the money on computer software and paid it back about two +weeks later. + ** Commodities and Currencies Ledger makes no assumptions about the commodities you use; it only @@ -277,11 +507,10 @@ specifiers: <example> $20.00 ; currency: twenty US dollars -USD 20 ; currency: the same 40 AAPL ; commodity: 40 shares of Apple stock -MD 60 ; currency: 60 Deutsch Mark +60 DM ; currency: 60 Deutsch Mark £50 ; currency: 50 British pounds -50e ; currency: 50 Euros (use symbol) +50e ; currency: 50 Euros (use appropriate symbol) </example> Ledger will examine the first use of any commodity to determine how @@ -391,8 +620,8 @@ To enable pricing reports, three options are possible: in this file. Also, this file will be read after all other ledger files are read, so that full history information is available for reports. - -**-T** :: + +**-O** :: Report commodity totals only, not their market value or basis cost. **-V** :: @@ -403,7 +632,7 @@ To enable pricing reports, three options are possible: at time of purchase. Thus, totals in the register and balance report reflect the total amount spent. -**-A** :: +**-G** :: Report commodities in terms of their net gain, which is: the market value minus the cost basis. A balance report using this option shows very quickly the performance of investments. @@ -427,7 +656,7 @@ To enable pricing reports, three options are possible: use: -p "$=0.00280112 AU" (or whatever the current exchange rate is). -Note that the =-B=, =-T=, =-V=, and =-A= are mutually exclusive. +Note that the =-B=, =-O=, =-V=, and =-G= are mutually exclusive. ** Accounts and Inventories @@ -653,7 +882,7 @@ automated transaction at the top of your ledger file: ; contents of the ledger. = ^Income: -= ^Expenses:Rent$ += ^Expenses:Rent$ = ^Expenses:Furnishings = ^Expenses:Business = ^Expenses:Taxes @@ -1155,12 +1384,38 @@ launches =vi= to let you confirm that the entry looks appropriate. **-M** :: When used with the "register" command, causes only monthly subtotals to appear. This can be useful for looking at spending patterns. - TODO: Accept an argument which specifies the period to use. + TODO: Accept an argument specifying the period to use. -**-G** :: - Modifies the output generated by -M to be friendly to programs like - Gnuplot. It strips away the commodity label, and outputs only two - columns: the date and the amount. +**-A** :: + Report totals in terms of the arithmetic mean (sum of all items + divided by the count). This does not work when multiple commodities + are used in the same account, in which case this option is ignored. + This option works both for balance reports, and for register reports + (where it displays the running total average). Be aware that in the + balance report, parent account totals reflect the arithmetic mean of + all the transactions -- not the mean average of the subaccount + totals. + +**-T** :: + Report totals in terms of the average deviation from the average + value (i.e., the trend). The final total will indicate the amount + over or above the average value which it is expected you will next + spend/earn. When spending is regular, the trend will very slowly + move to zero. + +**-X** :: + Report totals in terms of the expected value of the next + transaction. This is determined by adding the average deviation to + the average value. + +**-W** :: + Report totals in terms of a time-weighted trend. Whereas =-T= + reports the exact value trend irrespective of when the transactions + occurred, =-W= takes into account the time between entries. If a + transaction occurs shortly after another, it will not affect the + running trend as much as if it occurs very much later. This style + of reports always adds a null transaction for the current date, so + that a current lack of spending is taken into account. *** Commodity reporting options @@ -1170,8 +1425,8 @@ launches =vi= to let you confirm that the entry looks appropriate. in this file. Also, this file will be read after all other ledger files are read, so that full history information is available for reports. - -**-T** :: + +**-O** :: Report commodity totals only, not their market value or basis cost. **-V** :: @@ -1182,7 +1437,7 @@ launches =vi= to let you confirm that the entry looks appropriate. at time of purchase. Thus, totals in the register and balance report reflect the total amount spent. -**-A** :: +**-G** :: Report commodities in terms of their net gain, which is: the market value minus the cost basis. A balance report using this option shows very quickly the performance of investments. diff --git a/account.cc b/account.cc new file mode 100644 index 00000000..df57622b --- /dev/null +++ b/account.cc @@ -0,0 +1,91 @@ +#include "ledger.h" + +#include <sstream> +#include <deque> + +namespace ledger { + +unsigned long account_t::next_ident; + +account_t::~account_t() +{ + for (accounts_map::iterator i = accounts.begin(); + i != accounts.end(); + i++) + delete (*i).second; +} + +account_t * account_t::find_account(const std::string& ident, + const bool auto_create) +{ + accounts_map::const_iterator c = accounts_cache.find(ident); + if (c != accounts_cache.end()) + return (*c).second; + + accounts_map::const_iterator i = accounts.find(ident); + if (i != accounts.end()) + return (*i).second; + + static char buf[256]; + + std::string::size_type sep = ident.find(':'); + const char * first, * rest; + if (sep == std::string::npos) { + first = ident.c_str(); + rest = NULL; + } else { + std::strncpy(buf, ident.c_str(), sep); + buf[sep] = '\0'; + + first = buf; + rest = ident.c_str() + sep + 1; + } + + account_t * account; + + i = accounts.find(first); + if (i == accounts.end()) { + if (! auto_create) + return NULL; + account = new account_t(this, first); + accounts.insert(accounts_pair(first, account)); + } else { + account = (*i).second; + } + + if (rest) + account = account->find_account(rest, auto_create); + + accounts_cache.insert(accounts_pair(ident, account)); + + return account; +} + +bool account_t::remove_transaction(transaction_t * xact) +{ + for (transactions_list::iterator i = transactions.begin(); + i != transactions.end(); + i++) + if (*i == xact) { + transactions.erase(i); + return true; + } + + return false; +} + +std::string account_t::fullname() const +{ + const account_t * first = this; + std::string fullname = name; + + while (first->parent) { + first = first->parent; + if (! first->name.empty()) + fullname = first->name + ":" + fullname; + } + + return fullname; +} + +} // namespace ledger @@ -1,99 +1,21 @@ #include "ledger.h" -#include <sstream> -#include <cstdio> -#include <gmp.h> // GNU multi-precision library +#include "gmp.h" -namespace ledger { - -#define MAX_PRECISION 10 // must be 2 or higher - -////////////////////////////////////////////////////////////////////// -// -// The `amount' structure. Every transaction has an associated -// amount, which is represented by this structure. `amount' uses the -// GNU multi-precision library, allowing for arbitrarily large -// amounts. Each amount is a quantity of a certain commodity, with -// an optional price per-unit for that commodity at the time the -// amount was stated. -// -// To create an amount, for example: -// -// amount * cost = create_amount("50.2 MSFT @ $100.50"); -// - -class gmp_amount : public amount -{ - bool priced; - - mpz_t price; - commodity * price_comm; - - mpz_t quantity; - commodity * quantity_comm; - - gmp_amount(const gmp_amount& other) {} - gmp_amount& operator=(const gmp_amount& other) { return *this; } - - public: - gmp_amount() : priced(false), price_comm(NULL), quantity_comm(NULL) { - mpz_init(price); - mpz_init(quantity); - } - - virtual ~gmp_amount() { - mpz_clear(price); - mpz_clear(quantity); - } - - virtual commodity * commdty() const { - return quantity_comm; - } - - virtual void set_commdty(commodity * comm) { - quantity_comm = comm; - } +#define MAX_PRECISION 10 - virtual amount * copy() const; - virtual amount * value(const amount *) const; - virtual void set_value(const amount * val); - virtual amount * street(std::time_t * when = NULL, - bool use_history = false, - bool download = false) const; +#define MPZ(x) ((MP_INT *)(x)) - virtual bool has_price() const { - return priced; - } - virtual amount * per_item_price() const; - - virtual bool is_zero() const; - virtual bool is_negative() const; - virtual int compare(const amount * other) const; - - virtual void negate() { - mpz_ui_sub(quantity, 0, quantity); - } - virtual void credit(const amount * other); +#define INIT() if (! quantity) _init() - virtual void parse(const std::string& num); - virtual const std::string as_str(bool full_prec) const; - - friend amount * create_amount(const std::string& value, - const amount * cost); -}; +namespace ledger { -amount * create_amount(const std::string& value, const amount * cost) -{ - gmp_amount * a = new gmp_amount(); - a->parse(value); - if (cost) - a->set_value(cost); - return a; -} +commodity_t * amount_t::null_commodity = NULL; -static void round(mpz_t out, const mpz_t val, int prec) +static void mpz_round(mpz_t value, int precision) { mpz_t divisor; + mpz_t quotient; mpz_t remainder; @@ -101,29 +23,29 @@ static void round(mpz_t out, const mpz_t val, int prec) mpz_init(quotient); mpz_init(remainder); - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); - mpz_tdiv_qr(quotient, remainder, val, divisor); - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec - 1); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - precision); + mpz_tdiv_qr(quotient, remainder, value, divisor); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - precision - 1); mpz_mul_ui(divisor, divisor, 5); if (mpz_sgn(remainder) < 0) { mpz_ui_sub(divisor, 0, divisor); if (mpz_cmp(remainder, divisor) < 0) { - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - precision); mpz_add(remainder, divisor, remainder); mpz_ui_sub(remainder, 0, remainder); - mpz_add(out, val, remainder); + mpz_add(value, value, remainder); } else { - mpz_sub(out, val, remainder); + mpz_sub(value, value, remainder); } } else { if (mpz_cmp(remainder, divisor) >= 0) { - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - prec); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - precision); mpz_sub(remainder, divisor, remainder); - mpz_add(out, val, remainder); + mpz_add(value, value, remainder); } else { - mpz_sub(out, val, remainder); + mpz_sub(value, value, remainder); } } @@ -132,247 +54,391 @@ static void round(mpz_t out, const mpz_t val, int prec) mpz_clear(remainder); } -static void multiply(mpz_t out, const mpz_t l, const mpz_t r) +// destructor +void amount_t::_clear() { - mpz_t divisor; - - mpz_init(divisor); + mpz_clear(MPZ(quantity)); + delete (MP_INT *) quantity; +} - mpz_mul(out, l, r); +void amount_t::_init() +{ + quantity = new MP_INT; + mpz_init(MPZ(quantity)); +} - // The number is at double-precision right now, so rounding at - // precision 0 effectively means rounding to the ordinary - // precision. - round(out, out, 0); +void amount_t::_copy(const amount_t& amt) +{ + if (quantity) { + mpz_set(MPZ(quantity), MPZ(amt.quantity)); + } else { + quantity = new MP_INT; + mpz_init_set(MPZ(quantity), MPZ(amt.quantity)); + } + commodity = amt.commodity; + assert(commodity); +} - // after multiplying, truncate to the correct precision - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); - mpz_tdiv_q(out, out, divisor); +amount_t& amount_t::operator=(const std::string& value) +{ + std::istringstream str(value); + parse(str); + return *this; +} - mpz_clear(divisor); +// assignment operator +amount_t& amount_t::operator=(const amount_t& amt) +{ + if (amt.quantity) + _copy(amt); + return *this; } -amount * gmp_amount::copy() const +amount_t& amount_t::operator=(const int value) { - gmp_amount * new_amt = new gmp_amount(); + if (value == 0) { + if (quantity) { + _clear(); + quantity = NULL; + commodity = NULL; + } + } else { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + return *this; +} - mpz_set(new_amt->quantity, quantity); - new_amt->quantity_comm = quantity_comm; +amount_t& amount_t::operator=(const unsigned int value) +{ + if (value == 0) { + if (quantity) { + _clear(); + quantity = NULL; + commodity = NULL; + } + } else { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + return *this; +} - return new_amt; +amount_t& amount_t::operator=(const double value) +{ + if (value == 0.0) { + if (quantity) { + _clear(); + quantity = NULL; + commodity = NULL; + } + } else { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + return *this; } -amount * gmp_amount::per_item_price() const + +amount_t& amount_t::operator+=(const amount_t& amt) { - if (! priced) - return NULL; + if (amt.quantity) { + assert(commodity == amt.commodity); + INIT(); + mpz_add(MPZ(quantity), MPZ(quantity), MPZ(amt.quantity)); + } + return *this; +} - gmp_amount * new_amt = new gmp_amount(); +amount_t& amount_t::operator-=(const amount_t& amt) +{ + if (amt.quantity) { + assert(commodity == amt.commodity); + INIT(); + mpz_sub(MPZ(quantity), MPZ(quantity), MPZ(amt.quantity)); + } + return *this; +} - mpz_set(new_amt->quantity, price); - new_amt->quantity_comm = price_comm; +// unary negation +amount_t& amount_t::negate() +{ + if (quantity) + mpz_ui_sub(MPZ(quantity), 0, MPZ(quantity)); + return *this; +} - return new_amt; +// comparisons to zero +bool amount_t::operator<(const int num) const +{ + if (num == 0) { + return quantity ? mpz_sgn(MPZ(quantity)) < 0 : false; + } else { + std::string str; + std::ostringstream strstr(str); + strstr << num; + amount_t amt(strstr.str()); + return *this < amt; + } } -amount * gmp_amount::value(const amount * pr) const +bool amount_t::operator<=(const int num) const { - if (pr) { - const gmp_amount * p = dynamic_cast<const gmp_amount *>(pr); - assert(p); + if (num == 0) { + return quantity ? mpz_sgn(MPZ(quantity)) <= 0 : true; + } else { + std::string str; + std::ostringstream strstr(str); + strstr << num; + amount_t amt(strstr.str()); + return *this <= amt; + } +} - gmp_amount * new_amt = new gmp_amount(); +bool amount_t::operator>(const int num) const +{ + if (num == 0) { + return quantity ? mpz_sgn(MPZ(quantity)) > 0 : false; + } else { + std::string str; + std::ostringstream strstr(str); + strstr << num; + amount_t amt(strstr.str()); + return *this > amt; + } +} - multiply(new_amt->quantity, quantity, p->quantity); +bool amount_t::operator>=(const int num) const +{ + if (num == 0) { + return quantity ? mpz_sgn(MPZ(quantity)) >= 0 : true; + } else { + std::string str; + std::ostringstream strstr(str); + strstr << num; + amount_t amt(strstr.str()); + return *this >= amt; + } +} - // If the price we are multiplying by has no commodity, use the - // commodity of the current amount. - if (p->quantity_comm) - new_amt->quantity_comm = p->quantity_comm; - else - new_amt->quantity_comm = quantity_comm; +// comparisons between amounts +bool amount_t::operator<(const amount_t& amt) const +{ + if (! quantity) // equivalent to zero + return amt > 0; + if (! amt.quantity) // equivalent to zero + return *this < 0; + assert(commodity == amt.commodity); + return mpz_cmp(MPZ(quantity), MPZ(amt.quantity)) < 0; +} - if (new_amt->quantity_comm && - new_amt->quantity_comm->precision < MAX_PRECISION) - round(new_amt->quantity, new_amt->quantity, - new_amt->quantity_comm->precision); +bool amount_t::operator<=(const amount_t& amt) const +{ + if (! quantity) // equivalent to zero + return amt >= 0; + if (! amt.quantity) // equivalent to zero + return *this <= 0; + assert(commodity == amt.commodity); + return mpz_cmp(MPZ(quantity), MPZ(amt.quantity)) <= 0; +} - return new_amt; - } - else if (! priced) { - return copy(); - } - else { - gmp_amount * new_amt = new gmp_amount(); +bool amount_t::operator>(const amount_t& amt) const +{ + if (! quantity) // equivalent to zero + return amt < 0; + if (! amt.quantity) // equivalent to zero + return *this > 0; + assert(commodity == amt.commodity); + return mpz_cmp(MPZ(quantity), MPZ(amt.quantity)) > 0; +} - multiply(new_amt->quantity, quantity, price); +bool amount_t::operator>=(const amount_t& amt) const +{ + if (! quantity) // equivalent to zero + return amt <= 0; + if (! amt.quantity) // equivalent to zero + return *this >= 0; + assert(commodity == amt.commodity); + return mpz_cmp(MPZ(quantity), MPZ(amt.quantity)) >= 0; +} - new_amt->quantity_comm = price_comm; - if (new_amt->quantity_comm->precision < MAX_PRECISION) - round(new_amt->quantity, new_amt->quantity, - new_amt->quantity_comm->precision); +bool amount_t::operator==(const amount_t& amt) const +{ + if (commodity != amt.commodity) + return false; + assert(amt.quantity); + assert(quantity); + return mpz_cmp(MPZ(quantity), MPZ(amt.quantity)) == 0; +} - return new_amt; +amount_t::operator bool() const +{ + if (quantity) { + assert(commodity); + return mpz_sgn(MPZ(round().quantity)) != 0; + } else { + return false; } } -amount * gmp_amount::street(std::time_t * when, - bool use_history, bool download) const +amount_t amount_t::value(const std::time_t moment) const { - static std::time_t now = std::time(NULL); - if (! when) - when = &now; + if (quantity && ! (commodity->flags & COMMODITY_STYLE_NOMARKET)) + if (amount_t amt = commodity->value(moment)) + return (amt * *this).round(); - amount * amt = copy(); - - if (! amt->commdty()) - return amt; + return *this; +} - int max = 10; - while (--max >= 0) { - amount * price = amt->commdty()->price(when, use_history, download); - if (! price) - break; +amount_t& amount_t::operator*=(const amount_t& amt) +{ + if (! amt.quantity) + return *this; - amount * old = amt; - amt = amt->value(price); + INIT(); - if (amt->commdty() == old->commdty()) { - delete old; - break; - } + mpz_mul(MPZ(quantity), MPZ(quantity), MPZ(amt.quantity)); - delete old; - } + // Truncate to the recorded precision: MAX_PRECISION. + mpz_t divisor; + mpz_init(divisor); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); + mpz_tdiv_q(MPZ(quantity), MPZ(quantity), divisor); + mpz_clear(divisor); - return amt; + return *this; } -void gmp_amount::set_value(const amount * val) +amount_t& amount_t::operator/=(const amount_t& amt) { - assert(! priced); // don't specify the pricing twice! + if (! amt.quantity) + return *this; - const gmp_amount * v = dynamic_cast<const gmp_amount *>(val); - assert(v); + INIT(); - mpz_t quotient; - mpz_t remainder; - mpz_t addend; - - mpz_init(quotient); - mpz_init(remainder); - mpz_init(addend); - - mpz_ui_pow_ui(addend, 10, MAX_PRECISION); + mpz_t divisor; + mpz_init(divisor); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); - mpz_tdiv_qr(quotient, remainder, v->quantity, quantity); - mpz_mul(remainder, remainder, addend); - mpz_tdiv_q(remainder, remainder, quantity); - mpz_mul(quotient, quotient, addend); - mpz_add(quotient, quotient, remainder); - mpz_abs(quotient, quotient); + mpz_mul(MPZ(quantity), MPZ(quantity), divisor); + mpz_tdiv_q(MPZ(quantity), MPZ(quantity), MPZ(amt.quantity)); - priced = true; - mpz_set(price, quotient); - price_comm = v->quantity_comm; + mpz_clear(divisor); - mpz_clear(quotient); - mpz_clear(remainder); - mpz_clear(addend); + return *this; } -bool gmp_amount::is_zero() const +amount_t& amount_t::operator%=(const amount_t& amt) { - mpz_t copy; - mpz_init_set(copy, quantity); - if (quantity_comm && quantity_comm->precision < MAX_PRECISION) - round(copy, copy, quantity_comm->precision); - bool zero = mpz_sgn(copy) == 0; - mpz_clear(copy); - return zero; -} + if (! amt.quantity) + return *this; -bool gmp_amount::is_negative() const -{ - return mpz_sgn(quantity) < 0; + INIT(); + + mpz_t divisor; + mpz_init(divisor); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); + + mpz_mul(MPZ(quantity), MPZ(quantity), divisor); + mpz_tdiv_r(MPZ(quantity), MPZ(quantity), MPZ(amt.quantity)); + + mpz_clear(divisor); + + return *this; } -int gmp_amount::compare(const amount * other) const +amount_t amount_t::round(int precision) const { - amount * revalued = copy(); - amount * copied = other->copy(); - revalued->negate(); - copied->credit(revalued); - delete revalued; - int result = 1; - if (copied->is_zero()) - result = 0; - else if (copied->is_negative()) - result = -1; - delete copied; - return result; + if (! quantity) { + return *this; + } else { + amount_t temp = *this; + mpz_round(MPZ(temp.quantity), + precision == -1 ? commodity->precision : precision); + return temp; + } } -static std::string amount_to_str(const commodity * comm, const mpz_t val, - bool full_precision) +std::ostream& operator<<(std::ostream& out, const amount_t& amt) { - mpz_t temp; mpz_t quotient; mpz_t rquotient; mpz_t remainder; mpz_t divisor; - bool negative = false; - - mpz_init_set(temp, val); + if (! amt.quantity) + return out; mpz_init(quotient); mpz_init(rquotient); mpz_init(remainder); mpz_init(divisor); - if (comm == NULL) - full_precision = true; - - if (! full_precision && comm->precision < MAX_PRECISION) - round(temp, temp, comm->precision); + bool negative = false; mpz_ui_pow_ui(divisor, 10, MAX_PRECISION); - mpz_tdiv_qr(quotient, remainder, temp, divisor); + mpz_tdiv_qr(quotient, remainder, MPZ(amt.quantity), divisor); if (mpz_sgn(quotient) < 0 || mpz_sgn(remainder) < 0) negative = true; + mpz_abs(quotient, quotient); mpz_abs(remainder, remainder); - if (full_precision || comm->precision == MAX_PRECISION) { + if (amt.commodity->precision == MAX_PRECISION) { mpz_set(rquotient, remainder); } else { - assert(MAX_PRECISION - comm->precision > 0); - mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - comm->precision); + // Ensure the value is rounded to the commodity's precision before + // outputting it + mpz_round(MPZ(amt.quantity), amt.commodity->precision); + + assert(MAX_PRECISION - amt.commodity->precision > 0); + mpz_ui_pow_ui(divisor, 10, MAX_PRECISION - amt.commodity->precision); mpz_tdiv_qr(rquotient, remainder, remainder, divisor); } - std::ostringstream s; + bool odd_chars_in_symbol = false; - if (comm && comm->prefix) { - s << comm->symbol; - if (comm->separate) - s << " "; + for (const char * p = amt.commodity->symbol.c_str(); + *p; + p++) + if (std::isspace(*p) || std::isdigit(*p) || *p == '-' || *p == '.') { + odd_chars_in_symbol = true; + break; + } + + if (! (amt.commodity->flags & COMMODITY_STYLE_SUFFIXED)) { + if (odd_chars_in_symbol) + out << "\"" << amt.commodity->symbol << "\""; + else + out << amt.commodity->symbol; + if (amt.commodity->flags & COMMODITY_STYLE_SEPARATED) + out << " "; } if (negative) - s << "-"; + out << "-"; - if (mpz_sgn(quotient) == 0) - s << '0'; - else if (! comm || ! comm->thousands) - s << quotient; + if (mpz_sgn(quotient) == 0) { + out << '0'; + } + else if (! (amt.commodity->flags & COMMODITY_STYLE_THOUSANDS)) { + out << quotient; + } else { bool printed = false; + mpz_t temp; + mpz_init(temp); + // jww (2003-09-29): use a smarter starting value for `powers' - for (int powers = 27; powers >= 0; powers -= 3) { + for (int powers = 15; powers >= 0; powers -= 3) { mpz_ui_pow_ui(divisor, 10, powers); mpz_tdiv_q(temp, quotient, divisor); @@ -383,239 +449,271 @@ static std::string amount_to_str(const commodity * comm, const mpz_t val, mpz_tdiv_r(temp, temp, divisor); if (printed) { - s.width(3); - s.fill('0'); + out.width(3); + out.fill('0'); } - s << temp; + out << temp; if (powers > 0) { - s << (comm && comm->european ? '.' : ','); + out << ((amt.commodity->flags & COMMODITY_STYLE_EUROPEAN) ? '.' : ','); printed = true; } } - } - s << (comm && comm->european ? ',' : '.'); - - if (comm && (! full_precision || mpz_sgn(rquotient) == 0)) { - s.width(comm->precision); - s.fill('0'); - s << rquotient; - } else { - char buf[MAX_PRECISION + 1]; - gmp_sprintf(buf, "%Zd", rquotient); - - int width = std::strlen(buf); - char * p = buf + (width - 1); - - width = MAX_PRECISION - width; + mpz_clear(temp); + } - if (comm) { - while (p >= buf && *p == '0' && - (p - buf) >= (comm->precision - width)) - p--; - *(p + 1) = '\0'; - } + out << ((amt.commodity->flags & COMMODITY_STYLE_EUROPEAN) ? ',' : '.'); - s.width(width + std::strlen(buf)); - s.fill('0'); - s << buf; - } + out.width(amt.commodity->precision); + out.fill('0'); + out << rquotient; - if (comm && ! comm->prefix) { - if (comm->separate) - s << " "; - s << comm->symbol; + if (amt.commodity->flags & COMMODITY_STYLE_SUFFIXED) { + if (amt.commodity->flags & COMMODITY_STYLE_SEPARATED) + out << " "; + if (odd_chars_in_symbol) + out << "\"" << amt.commodity->symbol << "\""; + else + out << amt.commodity->symbol; } - mpz_clear(temp); mpz_clear(quotient); mpz_clear(rquotient); mpz_clear(remainder); mpz_clear(divisor); - return s.str(); + return out; } -const std::string gmp_amount::as_str(bool full_prec) const +amount_t::operator std::string() const { std::ostringstream s; - - s << amount_to_str(quantity_comm, quantity, full_prec); - - if (priced) { - s << " @ "; - s << amount_to_str(price_comm, price, full_prec); - } + s << *this; return s.str(); } -static void parse_number(mpz_t out, const std::string& number, - commodity * comm) +static inline char peek_next_nonws(std::istream& in) { - const char * num = number.c_str(); - - if (char * p = std::strchr(num, '/')) { - mpz_t numer; - mpz_t val; + char c = in.peek(); + while (! in.eof() && std::isspace(c)) { + in.get(c); + c = in.peek(); + } + return c; +} - // The number was specified as a numerator over denominator, such - // as 5250/100. This gives us the precision, and avoids any - // nastiness having to do with international numbering formats. +void parse_quantity(std::istream& in, std::string& value) +{ + char c = peek_next_nonws(in); + while (std::isdigit(c) || c == '-' || c == '.' || c == ',') { + in.get(c); + if (in.eof()) + break; + value += c; + c = in.peek(); + } +} - std::string numer_str(num, p - num); - mpz_init_set_str(numer, numer_str.c_str(), 10); - mpz_init(val); +void parse_commodity(std::istream& in, std::string& symbol) +{ + char c = peek_next_nonws(in); + if (c == '"') { + in.get(c); + c = in.peek(); + while (! in.eof() && c != '"') { + in.get(c); + if (c == '\\') + in.get(c); + symbol += c; + c = in.peek(); + } - int missing = MAX_PRECISION - (std::strlen(++p) - 1); - assert(missing > 0); - mpz_ui_pow_ui(val, 10, missing); + if (c == '"') + in.get(c); + else + assert(0); + } else { + while (! std::isspace(c) && ! std::isdigit(c) && c != '-' && c != '.') { + in.get(c); + if (in.eof()) + break; + symbol += c; + c = in.peek(); + } + } +} - mpz_mul(out, numer, val); +void amount_t::parse(std::istream& in, ledger_t * ledger) +{ + // The possible syntax for an amount is: + // + // [-]NUM[ ]SYM [@ AMOUNT] + // SYM[ ][-]NUM [@ AMOUNT] - mpz_clear(numer); - mpz_clear(val); - } - else { - static char buf[256]; + std::string symbol; + std::string quant; + unsigned int flags = COMMODITY_STYLE_DEFAULTS;; + unsigned int precision = MAX_PRECISION; - // The number is specified as the user desires, with the commodity - // telling us how to parse it. + if (! quantity) + _init(); - std::memset(buf, '0', 255); - std::strncpy(buf, num, std::strlen(num)); + char c = peek_next_nonws(in); + if (std::isdigit(c) || c == '.' || c == '-') { + parse_quantity(in, quant); - if (comm && comm->thousands) - while (char * t = std::strchr(buf, comm->european ? '.' : ',')) - do { *t = *(t + 1); } while (*(t++ + 1)); + char n; + if (! in.eof() && ((n = in.peek()) != '\n')) { + if (std::isspace(n)) + flags |= COMMODITY_STYLE_SEPARATED; - char * t = std::strchr(buf, (comm && comm->european) ? ',' : '.'); - if (! t) - t = buf + std::strlen(num); + parse_commodity(in, symbol); - for (int prec = 0; prec < MAX_PRECISION; prec++) { - *t = *(t + 1); - t++; + flags |= COMMODITY_STYLE_SUFFIXED; } - *t = '\0'; + } else { + parse_commodity(in, symbol); + + if (std::isspace(in.peek())) + flags |= COMMODITY_STYLE_SEPARATED; - mpz_set_str(out, buf, 10); + parse_quantity(in, quant); } -} -static commodity * parse_amount(mpz_t out, const char * num, - int matched, int * ovector, int base) -{ - static char buf[256]; + std::string::size_type last_comma = quant.rfind(','); + std::string::size_type last_period = quant.rfind('.'); - bool saw_commodity = false; - bool prefix = false; - bool separate = true; - bool thousands = true; - bool european = false; + if (last_comma != std::string::npos && last_period != std::string::npos) { + flags |= COMMODITY_STYLE_THOUSANDS; + if (last_comma > last_period) { + flags |= COMMODITY_STYLE_EUROPEAN; + precision = quant.length() - last_comma - 1; + } else { + precision = quant.length() - last_period - 1; + } + } + else if (last_comma != std::string::npos) { + flags |= COMMODITY_STYLE_EUROPEAN; + precision = quant.length() - last_comma - 1; + } + else if (last_period != std::string::npos) { + precision = quant.length() - last_period - 1; + } + else { + precision = 0; + quant = quant + ".0"; + } + assert(precision <= MAX_PRECISION); + + // Create the commodity if has not already been seen. + if (ledger) { + commodity = ledger->find_commodity(symbol, true); + commodity->flags |= flags; + if (precision > commodity->precision) + commodity->precision = precision; + } + else if (symbol.empty()) { + if (! null_commodity) { + commodity = null_commodity = new commodity_t(symbol, precision, flags); + } else { + commodity = null_commodity; + commodity->flags |= flags; + if (precision > commodity->precision) + commodity->precision = precision; + } + } + else { + commodity = new commodity_t(symbol, precision, flags); + } - std::string symbol; - int precision, result; + // The number is specified as the user desires, with the commodity + // flags telling how to parse it. - if (ovector[base * 2] >= 0) { - // A prefix symbol was found - saw_commodity = true; - prefix = true; - separate = ovector[(base + 2) * 2] != ovector[(base + 2) * 2 + 1]; - result = pcre_copy_substring(num, ovector, matched, base + 1, buf, 255); - assert(result >= 0); - symbol = buf; - } + int len = quant.length(); + int buf_len = len + MAX_PRECISION; + char * buf = new char[buf_len]; - // This is the value, and must be present - assert(ovector[(base + 3) * 2] >= 0); - result = pcre_copy_substring(num, ovector, matched, base + 3, buf, 255); - assert(result >= 0); + std::memset(buf, '0', buf_len - 1); + std::strncpy(buf, quant.c_str(), len); - // Where "thousands" markers used? Is it a european number? - if (char * p = std::strrchr(buf, ',')) { - if (std::strchr(p, '.')) - thousands = true; - else - european = true; - } + if (flags & COMMODITY_STYLE_THOUSANDS) + while (char * t = + std::strchr(buf, flags & COMMODITY_STYLE_EUROPEAN ? '.' : ',')) + do { *t = *(t + 1); } while (*(t++ + 1)); - // Determine the precision used - if (char * p = std::strchr(buf, european ? ',' : '.')) - precision = std::strlen(++p); - else if (char * p = std::strchr(buf, '/')) - precision = std::strlen(++p) - 1; - else - precision = 0; + char * t = std::strchr(buf, flags & COMMODITY_STYLE_EUROPEAN ? ',' : '.'); + if (! t) + t = buf + len; - // Parse the actual quantity - std::string value_str = buf; - - if (ovector[(base + 4) * 2] >= 0) { - // A suffix symbol was found - saw_commodity = true; - prefix = false; - separate = ovector[(base + 5) * 2] != ovector[(base + 5) * 2 + 1]; - result = pcre_copy_substring(num, ovector, matched, base + 6, buf, 255); - assert(result >= 0); - symbol = buf; - } - - commodity * comm = NULL; - if (saw_commodity) { - commodities_map_iterator item = - main_ledger->commodities.find(symbol.c_str()); - if (item == main_ledger->commodities.end()) - comm = new commodity(symbol, prefix, separate, thousands, - european, precision); - else - comm = (*item).second; + for (int prec = 0; prec < MAX_PRECISION; prec++) { + *t = *(t + 1); + t++; } + *t = '\0'; - parse_number(out, value_str.c_str(), comm); + mpz_set_str(MPZ(quantity), buf, 10); - return comm; + delete[] buf; } -void gmp_amount::parse(const std::string& number) +static char buf[4096]; + +void amount_t::write_quantity(std::ostream& out) const { - // Compile the regular expression used for parsing amounts - static pcre * re = NULL; - if (! re) { - const char *error; - int erroffset; - static const std::string amount_re = - "(([^-0-9/., ]+)(\\s*))?([-0-9/.,]+)((\\s*)([^-0-9/., @]+))?"; - const std::string regexp = - "^" + amount_re + "(\\s*@\\s*" + amount_re + ")?$"; - re = pcre_compile(regexp.c_str(), 0, &error, &erroffset, NULL); + unsigned short len; + if (quantity) { + mpz_get_str(buf, 10, MPZ(quantity)); + len = std::strlen(buf); + assert(len); + out.write((char *)&len, sizeof(len)); + out.write(buf, len); + } else { + len = 0; + out.write((char *)&len, sizeof(len)); } +} - int ovector[60]; - int matched; - - matched = pcre_exec(re, NULL, number.c_str(), number.length(), - 0, 0, ovector, 60); - if (matched > 0) { - quantity_comm = parse_amount(quantity, number.c_str(), matched, - ovector, 1); - - // If the following succeeded, then we have a price - if (ovector[8 * 2] >= 0) { - priced = true; - price_comm = parse_amount(price, number.c_str(), matched, - ovector, 9); - } +void amount_t::read_quantity(std::istream& in) +{ + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + if (! quantity) + _init(); + mpz_set_str(MPZ(quantity), buf, 10); } else { - std::cerr << "Failed to parse amount: " << number << std::endl; + if (quantity) + _clear(); + quantity = NULL; } } -void gmp_amount::credit(const amount * value) +void (*commodity_t::updater)(commodity_t * commodity, + const std::time_t date, + const amount_t& price, + const std::time_t moment) = NULL; + +amount_t commodity_t::value(const std::time_t moment) { - const gmp_amount * val = dynamic_cast<const gmp_amount *>(value); - assert(quantity_comm == val->quantity_comm); - mpz_add(quantity, quantity, val->quantity); + std::time_t age = 0; + amount_t price; + + if (updater) + updater(this, age, price, moment); + + for (history_map::reverse_iterator i = history.rbegin(); + i != history.rend(); + i++) + if (moment == 0 || std::difftime(moment, (*i).first) >= 0) { + age = (*i).first; + price = (*i).second; + break; + } + + return price; } } // namespace ledger diff --git a/balance.cc b/balance.cc new file mode 100644 index 00000000..5d806080 --- /dev/null +++ b/balance.cc @@ -0,0 +1,107 @@ +#include "ledger.h" +#include "balance.h" + +namespace ledger { + +amount_t balance_t::amount(const commodity_t * commodity) const +{ + if (! commodity) { + if (amounts.size() == 1) { + amounts_map::const_iterator i = amounts.begin(); + return (*i).second; + } + } + else if (amounts.size() > 0) { + amounts_map::const_iterator i = amounts.find(commodity); + if (i != amounts.end()) + return (*i).second; + } + return amount_t(); +} + +#if 0 +balance_t balance_t::round() const +{ + balance_t temp; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + temp += (*i).second.round(); + + return temp; +} +#endif + +balance_t balance_t::value(const std::time_t moment) const +{ + balance_t temp; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + temp += (*i).second.value(moment); + +#if 1 + return temp; +#else + return temp.round(); +#endif +} + +void balance_t::write(std::ostream& out, + const int first_width, + const int latter_width) const +{ + bool first = true; + int lwidth = latter_width; + + if (lwidth == -1) + lwidth = first_width; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) { + if ((*i).second) { + int width; + + if (! first) { + out << std::endl; + width = lwidth; + } else { + first = false; + width = first_width; + } + + out.width(width); + out.fill(' '); + out << std::right << std::string((*i).second); + } + } + + if (first) { + out.width(first_width); + out.fill(' '); + out << std::right << "0"; + } +} + + +balance_pair_t::balance_pair_t(const transaction_t& xact) + : quantity(xact.amount), cost(xact.cost) {} + +balance_pair_t& balance_pair_t::operator+=(const transaction_t& xact) +{ + quantity += xact.amount; + cost += xact.cost; + return *this; +} + +balance_pair_t& balance_pair_t::operator-=(const transaction_t& xact) +{ + quantity -= xact.amount; + cost -= xact.cost; + return *this; +} + +} // namespace ledger diff --git a/balance.h b/balance.h new file mode 100644 index 00000000..3ecfd962 --- /dev/null +++ b/balance.h @@ -0,0 +1,594 @@ +#ifndef _BALANCE_H +#define _BALANCE_H + +#include "ledger.h" + +namespace ledger { + +typedef std::map<const commodity_t *, amount_t> amounts_map; +typedef std::pair<const commodity_t *, amount_t> amounts_pair; + +class balance_t +{ + public: + amounts_map amounts; + + bool valid() const { + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if (! (*i).second.valid()) + return false; + return true; + } + + // constructors + balance_t() {} + balance_t(const balance_t& bal) { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this += (*i).second; + } + balance_t(const amount_t& amt) { + *this += amt; + } + balance_t(const int value) { + *this += amount_t(value); + } + balance_t(const unsigned int value) { + *this += amount_t(value); + } + balance_t(const double value) { + *this += amount_t(value); + } + + // destructor + ~balance_t() {} + + // assignment operator + balance_t& operator=(const balance_t& bal) { + amounts.clear(); + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this += (*i).second; + return *this; + } + balance_t& operator=(const amount_t& amt) { + amounts.clear(); + *this += amt; + return *this; + } + balance_t& operator=(const int value) { + amounts.clear(); + *this += amount_t(value); + return *this; + } + balance_t& operator=(const unsigned int value) { + amounts.clear(); + *this += amount_t(value); + return *this; + } + balance_t& operator=(const double value) { + amounts.clear(); + *this += amount_t(value); + return *this; + } + + // in-place arithmetic + balance_t& operator+=(const balance_t& bal) { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this += (*i).second; + return *this; + } + balance_t& operator+=(const amount_t& amt) { + amounts_map::iterator i = amounts.find(amt.commodity); + if (i != amounts.end()) + (*i).second += amt; + else if (amt) + amounts.insert(amounts_pair(amt.commodity, amt)); + return *this; + } + balance_t& operator-=(const balance_t& bal) { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this -= (*i).second; + return *this; + } + balance_t& operator-=(const amount_t& amt) { + amounts_map::iterator i = amounts.find(amt.commodity); + if (i != amounts.end()) + (*i).second -= amt; + else if (amt) + amounts.insert(amounts_pair(amt.commodity, amt)); + return *this; + } + + // simple arithmetic + balance_t operator+(const balance_t& bal) const { + balance_t temp = *this; + temp += bal; + return temp; + } + balance_t operator+(const amount_t& amt) const { + balance_t temp = *this; + temp += amt; + return temp; + } + balance_t operator-(const balance_t& bal) const { + balance_t temp = *this; + temp -= bal; + return temp; + } + balance_t operator-(const amount_t& amt) const { + balance_t temp = *this; + temp -= amt; + return temp; + } + + // multiplication and divide + balance_t& operator*=(const balance_t& bal) { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this *= (*i).second; + return *this; + } + balance_t& operator*=(const amount_t& amt) { + // Multiplying by the null commodity causes all amounts to be + // increased by the same factor. + if (amt.commodity->symbol.empty()) { + for (amounts_map::iterator i = amounts.begin(); + i != amounts.end(); + i++) + (*i).second *= amt; + } else { + amounts_map::iterator i = amounts.find(amt.commodity); + if (i != amounts.end()) + (*i).second *= amt; + } + return *this; + } + + balance_t& operator/=(const balance_t& bal) { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + *this /= (*i).second; + return *this; + } + balance_t& operator/=(const amount_t& amt) { + // Dividing by the null commodity causes all amounts to be + // increased by the same factor. + if (amt.commodity->symbol.empty()) { + for (amounts_map::iterator i = amounts.begin(); + i != amounts.end(); + i++) + (*i).second /= amt; + } else { + amounts_map::iterator i = amounts.find(amt.commodity); + if (i != amounts.end()) + (*i).second /= amt; + } + return *this; + } + + // multiplication and divide + balance_t operator*(const balance_t& bal) const { + balance_t temp = *this; + temp *= bal; + return temp; + } + balance_t operator*(const amount_t& amt) const { + balance_t temp = *this; + temp *= amt; + return temp; + } + balance_t operator/(const balance_t& bal) const { + balance_t temp = *this; + temp /= bal; + return temp; + } + balance_t operator/(const amount_t& amt) const { + balance_t temp = *this; + temp /= amt; + return temp; + } + + // comparison + bool operator<(const balance_t& bal) const { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + if (! (amount((*i).first) < (*i).second)) + return false; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if (! ((*i).second < bal.amount((*i).first))) + return false; + + return true; + } + bool operator<=(const balance_t& bal) const { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + if (! (amount((*i).first) <= (*i).second)) + return false; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if (! ((*i).second <= bal.amount((*i).first))) + return false; + + return true; + } + bool operator<(const amount_t& amt) const { + return amount(amt.commodity) < amt; + } + bool operator<=(const amount_t& amt) const { + return amount(amt.commodity) <= amt; + } + + bool operator>(const balance_t& bal) const { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + if (! (amount((*i).first) > (*i).second)) + return false; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if (! ((*i).second > bal.amount((*i).first))) + return false; + + return true; + } + bool operator>=(const balance_t& bal) const { + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + if (! (amount((*i).first) >= (*i).second)) + return false; + + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if (! ((*i).second >= bal.amount((*i).first))) + return false; + + return true; + } + bool operator>(const amount_t& amt) const { + return amount(amt.commodity) > amt; + } + bool operator>=(const amount_t& amt) const { + return amount(amt.commodity) >= amt; + } + + // unary negation + balance_t& negate() { + for (amounts_map::iterator i = amounts.begin(); + i != amounts.end(); + i++) + (*i).second.negate(); + return *this; + } + balance_t negated() const { + balance_t temp = *this; + temp.negate(); + return temp; + } + balance_t operator-() const { + return negated(); + } + + // test for non-zero (use ! for zero) + operator bool() const { + for (amounts_map::const_iterator i = amounts.begin(); + i != amounts.end(); + i++) + if ((*i).second) + return true; + return false; + } + + amount_t amount(const commodity_t * commodity = NULL) const; +#if 0 + balance_t round() const; +#endif + balance_t value(const std::time_t moment) const; + + void write(std::ostream& out, + const int first_width, + const int latter_width = -1) const; +}; + +inline balance_t abs(const balance_t& bal) { + balance_t temp; + for (amounts_map::const_iterator i = bal.amounts.begin(); + i != bal.amounts.end(); + i++) + temp += abs((*i).second); + return temp; +} + +#ifdef DEBUG +inline std::ostream& operator<<(std::ostream& out, const balance_t& bal) { + bal.write(out, 12); + return out; +} +#endif + + +class balance_pair_t +{ + public: + balance_t quantity; + balance_t cost; + + bool valid() const { + return quantity.valid() && cost.valid(); + } + + // constructors + balance_pair_t() {} + balance_pair_t(const balance_pair_t& bal_pair) + : quantity(bal_pair.quantity), cost(bal_pair.cost) {} + balance_pair_t(const balance_t& _quantity, const balance_t& _cost) + : quantity(_quantity), cost(_cost) {} + balance_pair_t(const balance_t& _quantity) + : quantity(_quantity), cost(_quantity) {} + balance_pair_t(const amount_t& _quantity, const amount_t& _cost) + : quantity(_quantity), cost(_cost) {} + balance_pair_t(const amount_t& _quantity) + : quantity(_quantity), cost(_quantity) {} + balance_pair_t(const int value) + : quantity(value), cost(value) {} + balance_pair_t(const unsigned int value) + : quantity(value), cost(value) {} + balance_pair_t(const double value) + : quantity(value), cost(value) {} + balance_pair_t(const transaction_t& xact); + + // destructor + ~balance_pair_t() {} + + // assignment operator + balance_pair_t& operator=(const balance_pair_t& bal_pair) { + quantity = bal_pair.quantity; + cost = bal_pair.cost; + return *this; + } + balance_pair_t& operator=(const balance_t& bal) { + quantity = cost = bal; + return *this; + } + balance_pair_t& operator=(const amount_t& amt) { + quantity = cost = amt; + return *this; + } + balance_pair_t& operator=(const int value) { + quantity = cost = amount_t(value); + return *this; + } + balance_pair_t& operator=(const unsigned int value) { + quantity = cost = amount_t(value); + return *this; + } + balance_pair_t& operator=(const double value) { + quantity = cost = amount_t(value); + return *this; + } + + // in-place arithmetic + balance_pair_t& operator+=(const balance_pair_t& bal_pair) { + quantity += bal_pair.quantity; + cost += bal_pair.cost; + return *this; + } + balance_pair_t& operator+=(const balance_t& bal) { + quantity += bal; + cost += bal; + return *this; + } + balance_pair_t& operator+=(const amount_t& amt) { + quantity += amt; + cost += amt; + return *this; + } + balance_pair_t& operator+=(const transaction_t& xact); + + balance_pair_t& operator-=(const balance_pair_t& bal_pair) { + quantity -= bal_pair.quantity; + cost -= bal_pair.cost; + return *this; + } + balance_pair_t& operator-=(const balance_t& bal) { + quantity -= bal; + cost -= bal; + return *this; + } + balance_pair_t& operator-=(const amount_t& amt) { + quantity -= amt; + cost -= amt; + return *this; + } + balance_pair_t& operator-=(const transaction_t& xact); + + // simple arithmetic + balance_pair_t operator+(const balance_pair_t& bal_pair) const { + balance_pair_t temp = *this; + temp += bal_pair; + return temp; + } + balance_pair_t operator+(const balance_t& bal) const { + balance_pair_t temp = *this; + temp += bal; + return temp; + } + balance_pair_t operator+(const amount_t& amt) const { + balance_pair_t temp = *this; + temp += amt; + return temp; + } + + balance_pair_t operator-(const balance_pair_t& bal_pair) const { + balance_pair_t temp = *this; + temp -= bal_pair; + return temp; + } + balance_pair_t operator-(const balance_t& bal) const { + balance_pair_t temp = *this; + temp -= bal; + return temp; + } + balance_pair_t operator-(const amount_t& amt) const { + balance_pair_t temp = *this; + temp -= amt; + return temp; + } + + // multiplication and division + balance_pair_t& operator*=(const balance_pair_t& bal_pair) { + quantity *= bal_pair.quantity; + cost *= bal_pair.quantity; + return *this; + } + balance_pair_t& operator*=(const balance_t& bal) { + quantity *= bal; + cost *= bal; + return *this; + } + balance_pair_t& operator*=(const amount_t& amt) { + quantity *= amt; + cost *= amt; + return *this; + } + + balance_pair_t& operator/=(const balance_pair_t& bal_pair) { + quantity /= bal_pair.quantity; + cost /= bal_pair.quantity; + return *this; + } + balance_pair_t& operator/=(const balance_t& bal) { + quantity /= bal; + cost /= bal; + return *this; + } + balance_pair_t& operator/=(const amount_t& amt) { + quantity /= amt; + cost /= amt; + return *this; + } + + balance_pair_t operator*(const balance_pair_t& bal_pair) const { + balance_pair_t temp = *this; + temp *= bal_pair; + return temp; + } + balance_pair_t operator*(const balance_t& bal) const { + balance_pair_t temp = *this; + temp *= bal; + return temp; + } + balance_pair_t operator*(const amount_t& amt) const { + balance_pair_t temp = *this; + temp *= amt; + return temp; + } + + balance_pair_t operator/(const balance_pair_t& bal_pair) const { + balance_pair_t temp = *this; + temp /= bal_pair; + return temp; + } + balance_pair_t operator/(const balance_t& bal) const { + balance_pair_t temp = *this; + temp /= bal; + return temp; + } + balance_pair_t operator/(const amount_t& amt) const { + balance_pair_t temp = *this; + temp /= amt; + return temp; + } + + // comparison + bool operator<(const balance_pair_t& bal_pair) const { + return quantity < bal_pair.quantity; + } + bool operator<(const balance_t& bal) const { + return quantity < bal; + } + bool operator<(const amount_t& amt) const { + return quantity < amt; + } + bool operator<=(const balance_pair_t& bal_pair) const { + return quantity <= bal_pair.quantity; + } + bool operator<=(const balance_t& bal) const { + return quantity <= bal; + } + bool operator<=(const amount_t& amt) const { + return quantity <= amt; + } + + bool operator>(const balance_pair_t& bal_pair) const { + return quantity > bal_pair.quantity; + } + bool operator>(const balance_t& bal) const { + return quantity > bal; + } + bool operator>(const amount_t& amt) const { + return quantity > amt; + } + bool operator>=(const balance_pair_t& bal_pair) const { + return quantity >= bal_pair.quantity; + } + bool operator>=(const balance_t& bal) const { + return quantity >= bal; + } + bool operator>=(const amount_t& amt) const { + return quantity >= amt; + } + + // unary negation + balance_pair_t& negate() { + quantity.negate(); + cost.negate(); + return *this; + } + balance_pair_t negated() const { + balance_pair_t temp = *this; + temp.negate(); + return temp; + } + balance_pair_t operator-() const { + return negated(); + } + + // test for non-zero (use ! for zero) + operator bool() const { + return quantity; + } +}; + +inline balance_pair_t abs(const balance_pair_t& bal_pair) { + balance_pair_t temp; + temp.quantity = abs(bal_pair.quantity); + temp.cost = abs(bal_pair.cost); + return temp; +} + +} // namespace ledger + +#endif // _BALANCE_H diff --git a/binary.cc b/binary.cc new file mode 100644 index 00000000..1ed7f2c5 --- /dev/null +++ b/binary.cc @@ -0,0 +1,621 @@ +#include "ledger.h" +#include "textual.h" + +#include <vector> +#include <fstream> +#include <sstream> +#include <cstring> +#include <ctime> +#include <cctype> + +#include <sys/stat.h> + +#define TIMELOG_SUPPORT 1 + +namespace ledger { + + unsigned long magic_number = 0xFFEED765; +static unsigned long format_version = 0x00020008; + +static char buf[4096]; + +static std::vector<account_t *> accounts; +static std::vector<commodity_t *> commodities; + +static unsigned long ident; +static unsigned long c_ident; + + +void read_binary_amount(std::istream& in, amount_t& amt) +{ + unsigned long id; + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1001); + } +#endif + + in.read((char *)&id, sizeof(id)); + if (id == 0xffffffff) + amt.commodity = NULL; + else + amt.commodity = commodities[id]; + + amt.read_quantity(in); + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1002); + } +#endif +} + +transaction_t * read_binary_transaction(std::istream& in, entry_t * entry) +{ + transaction_t * xact = new transaction_t(entry, NULL); + + unsigned long id; + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1003); + } +#endif + + in.read((char *)&id, sizeof(id)); + xact->account = accounts[id]; + xact->account->add_transaction(xact); + + read_binary_amount(in, xact->amount); + read_binary_amount(in, xact->cost); + + in.read((char *)&xact->flags, sizeof(xact->flags)); + + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + xact->note = buf; + } + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1004); + } +#endif + + return xact; +} + +entry_t * read_binary_entry(std::istream& in, ledger_t * ledger) +{ + entry_t * entry = new entry_t; + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1005); + } +#endif + + in.read((char *)&entry->date, sizeof(entry->date)); + in.read((char *)&entry->state, sizeof(entry->state)); + + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + entry->code = buf; + } + + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + entry->payee = buf; + } + + unsigned long count; + in.read((char *)&count, sizeof(count)); + + for (int i = count; --i >= 0; ) { + transaction_t * xact = read_binary_transaction(in, entry); + entry->transactions.push_back(xact); + } + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1006); + } +#endif + + return entry; +} + +commodity_t * read_binary_commodity(std::istream& in) +{ + unsigned long id; + + commodity_t * commodity = new commodity_t; + commodities.push_back(commodity); + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1007); + } +#endif + + in.read((char *)&id, sizeof(id)); + commodity->ident = id; + assert(id == commodities.size() - 1); + + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + commodity->symbol = buf; + } + + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + commodity->name = buf; + } + + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + commodity->note = buf; + } + + in.read((char *)&commodity->precision, sizeof(commodity->precision)); + in.read((char *)&commodity->flags, sizeof(commodity->flags)); + + unsigned long count; + in.read((char *)&count, sizeof(count)); + + for (int i = count; --i >= 0; ) { + std::time_t when; + in.read((char *)&when, sizeof(std::time_t)); + amount_t amt; + read_binary_amount(in, amt); + commodity->history.insert(history_pair(when, amt)); + } + + read_binary_amount(in, commodity->conversion); + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1008); + } +#endif + + return commodity; +} + +account_t * read_binary_account(std::istream& in, account_t * master = NULL) +{ + unsigned long id; + + account_t * acct = new account_t(NULL); + accounts.push_back(acct); + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1009); + } +#endif + + in.read((char *)&id, sizeof(id)); + acct->ident = id; + assert(id == accounts.size() - 1); + + in.read((char *)&id, sizeof(id)); + if (id == 0xffffffff) + acct->parent = NULL; + else + acct->parent = accounts[id]; + + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + acct->name = buf; + } + + in.read((char *)&len, sizeof(len)); + if (len) { + in.read(buf, len); + buf[len] = '\0'; + acct->note = buf; + } + + in.read((char *)&len, sizeof(len)); + + // If all of the subaccounts will be added to a different master + // account, throw away what we've learned about the recorded + // ledger's own master account. + + if (master) { + delete acct; + acct = master; + } + + for (int i = 0; i < len; i++) { + account_t * child = read_binary_account(in); + child->parent = acct; + acct->add_account(child); + } + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1010); + } +#endif + + return acct; +} + +unsigned int read_binary_ledger(std::istream& in, const std::string& leader, + ledger_t *& ledger, account_t * master) +{ + ident = 0; + c_ident = 0; + + unsigned long magic; + in.read((char *)&magic, sizeof(magic)); + if (magic != magic_number) + return NULL; + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1011); + } +#endif + + unsigned long this_ver; + in.read((char *)&this_ver, sizeof(this_ver)); + if (this_ver != format_version) + return NULL; + + unsigned short len; + in.read((char *)&len, sizeof(len)); + if (! len) + return NULL; + in.read(buf, len); + + if (leader != buf) + return NULL; + + if (! ledger) + ledger = new ledger_t; + + in.read((char *)&len, sizeof(len)); + + for (int i = len; --i >= 0; ) { + in.read((char *)&len, sizeof(len)); + assert(len); + in.read(buf, len); + buf[len] = '\0'; + + ledger->sources.push_back(buf); + + std::time_t old_mtime; + struct stat info; + in.read((char *)&old_mtime, sizeof(old_mtime)); + stat(buf, &info); + if (info.st_mtime > old_mtime) + return NULL; + } + + ledger->master = read_binary_account(in, master); + + unsigned long count; + in.read((char *)&count, sizeof(count)); + + for (int i = count; --i >= 0; ) { + commodity_t * commodity = read_binary_commodity(in); + std::pair<commodities_map::iterator, bool> result + = ledger->commodities.insert(commodities_pair(commodity->symbol, + commodity)); + assert(result.second || master); + } + + in.read((char *)&count, sizeof(count)); + + for (int i = count; --i >= 0; ) { + entry_t * entry = read_binary_entry(in, ledger); + ledger->entries.push_back(entry); + } + +#ifdef DEBUG + { + unsigned short guard; + in.read((char *)&guard, sizeof(guard)); + assert(guard == 0x1012); + } +#endif + + accounts.clear(); + commodities.clear(); + + return count; +} + + +void write_binary_amount(std::ostream& out, const amount_t& amt) +{ +#ifdef DEBUG + { + unsigned short guard = 0x1001; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + if (amt.commodity) { + out.write((char *)&amt.commodity->ident, sizeof(amt.commodity->ident)); + } else { + unsigned long end = 0xffffffff; + out.write((char *)&end, sizeof(end)); + } + + amt.write_quantity(out); + +#ifdef DEBUG + { + unsigned short guard = 0x1002; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +void write_binary_transaction(std::ostream& out, transaction_t * xact) +{ +#ifdef DEBUG + { + unsigned short guard = 0x1003; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + out.write((char *)&xact->account->ident, sizeof(xact->account->ident)); + write_binary_amount(out, xact->amount); + write_binary_amount(out, xact->cost); + out.write((char *)&xact->flags, sizeof(xact->flags)); + + unsigned short len = xact->note.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(xact->note.c_str(), len); + +#ifdef DEBUG + { + unsigned short guard = 0x1004; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +void write_binary_entry(std::ostream& out, entry_t * entry) +{ +#ifdef DEBUG + { + unsigned short guard = 0x1005; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + out.write((char *)&entry->date, sizeof(entry->date)); + out.write((char *)&entry->state, sizeof(entry->state)); + + unsigned short len = entry->code.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(entry->code.c_str(), len); + + len = entry->payee.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(entry->payee.c_str(), len); + + unsigned long count = entry->transactions.size(); + out.write((char *)&count, sizeof(count)); + + for (transactions_list::const_iterator i = entry->transactions.begin(); + i != entry->transactions.end(); + i++) + write_binary_transaction(out, *i); + +#ifdef DEBUG + { + unsigned short guard = 0x1006; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +void write_binary_commodity(std::ostream& out, commodity_t * commodity) +{ +#ifdef DEBUG + { + unsigned short guard = 0x1007; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + out.write((char *)&c_ident, sizeof(c_ident)); + commodity->ident = c_ident; + ++c_ident; + + unsigned short len = commodity->symbol.length(); + out.write((char *)&len, sizeof(len)); + out.write(commodity->symbol.c_str(), len); + + len = commodity->name.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(commodity->name.c_str(), len); + + len = commodity->note.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(commodity->note.c_str(), len); + + out.write((char *)&commodity->precision, sizeof(commodity->precision)); + out.write((char *)&commodity->flags, sizeof(commodity->flags)); + + unsigned long count = commodity->history.size(); + out.write((char *)&count, sizeof(count)); + + for (history_map::const_iterator i = commodity->history.begin(); + i != commodity->history.end(); + i++) { + out.write((char *)&((*i).first), sizeof(std::time_t)); + write_binary_amount(out, (*i).second); + } + + write_binary_amount(out, commodity->conversion); + +#ifdef DEBUG + { + unsigned short guard = 0x1008; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +void write_binary_account(std::ostream& out, account_t * account) +{ +#ifdef DEBUG + { + unsigned short guard = 0x1009; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + out.write((char *)&ident, sizeof(ident)); + account->ident = ident; + ++ident; + + if (account->parent) { + out.write((char *)&account->parent->ident, sizeof(account->parent->ident)); + } else { + unsigned long end = 0xffffffff; + out.write((char *)&end, sizeof(end)); + } + + unsigned short len = account->name.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(account->name.c_str(), len); + + len = account->note.length(); + out.write((char *)&len, sizeof(len)); + if (len) + out.write(account->note.c_str(), len); + + len = account->accounts.size(); + out.write((char *)&len, sizeof(len)); + + for (accounts_map::iterator i = account->accounts.begin(); + i != account->accounts.end(); + i++) + write_binary_account(out, (*i).second); + +#ifdef DEBUG + { + unsigned short guard = 0x1010; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +void write_binary_ledger(std::ostream& out, ledger_t * ledger, + const std::string& leader) +{ + out.write((char *)&magic_number, sizeof(magic_number)); + +#ifdef DEBUG + { + unsigned short guard = 0x1011; + out.write((char *)&guard, sizeof(guard)); + } +#endif + + out.write((char *)&format_version, sizeof(format_version)); + + unsigned short len = leader.length(); + assert(len > 0); + out.write((char *)&len, sizeof(len)); + out.write(leader.c_str(), len); + + len = ledger->sources.size(); + out.write((char *)&len, sizeof(len)); + + for (std::list<std::string>::const_iterator i = ledger->sources.begin(); + i != ledger->sources.end(); + i++) { + len = (*i).length(); + out.write((char *)&len, sizeof(len)); + assert(len); + out.write((*i).c_str(), len); + struct stat info; + stat((*i).c_str(), &info); + out.write((char *)&info.st_mtime, sizeof(info.st_mtime)); + } + + write_binary_account(out, ledger->master); + + unsigned long count = ledger->commodities.size(); + out.write((char *)&count, sizeof(count)); + + for (commodities_map::const_iterator i = ledger->commodities.begin(); + i != ledger->commodities.end(); + i++) + write_binary_commodity(out, (*i).second); + + count = ledger->entries.size(); + out.write((char *)&count, sizeof(count)); + + for (entries_list::const_iterator i = ledger->entries.begin(); + i != ledger->entries.end(); + i++) + write_binary_entry(out, *i); + +#ifdef DEBUG + { + unsigned short guard = 0x1012; + out.write((char *)&guard, sizeof(guard)); + } +#endif +} + +} // namespace ledger diff --git a/binary.h b/binary.h new file mode 100644 index 00000000..4c7f3a23 --- /dev/null +++ b/binary.h @@ -0,0 +1,21 @@ +#ifndef _BINARY_H +#define _BINARY_H + +#include "ledger.h" + +namespace ledger { + +extern unsigned long magic_number; + +extern unsigned int read_binary_ledger(std::istream& in, + const std::string& leader, + ledger_t *& book, + account_t * master = NULL); + +extern void write_binary_ledger(std::ostream& out, + ledger_t * ledger, + const std::string& leader); + +} // namespace ledger + +#endif // _BINARY_H diff --git a/constraint.cc b/constraint.cc new file mode 100644 index 00000000..1248c2b1 --- /dev/null +++ b/constraint.cc @@ -0,0 +1,219 @@ +#include "constraint.h" + +#include <pcre.h> + +namespace ledger { + +mask_t::mask_t(const std::string& pat) : exclude(false) +{ + const char * p = pat.c_str(); + if (*p == '-') { + exclude = true; + p++; + while (std::isspace(*p)) + p++; + } + else if (*p == '+') { + p++; + while (std::isspace(*p)) + p++; + } + pattern = p; + + const char *error; + int erroffset; + regexp = pcre_compile(pattern.c_str(), PCRE_CASELESS, + &error, &erroffset, NULL); + if (! regexp) + std::cerr << "Warning: Failed to compile regexp: " << pattern + << std::endl; +} + +mask_t::mask_t(const mask_t& m) : exclude(m.exclude), pattern(m.pattern) +{ + const char *error; + int erroffset; + regexp = pcre_compile(pattern.c_str(), PCRE_CASELESS, + &error, &erroffset, NULL); + assert(regexp); +} + +bool mask_t::match(const std::string& str) const +{ + static int ovec[30]; + int result = pcre_exec((pcre *)regexp, NULL, + str.c_str(), str.length(), 0, 0, ovec, 30); + return result >= 0 && ! exclude; +} + +mask_t::~mask_t() { + pcre_free((pcre *)regexp); +} + +bool matches(const masks_list& regexps, const std::string& str, + bool * by_exclusion) +{ + if (regexps.empty()) + return false; + + bool match = false; + bool definite = false; + + for (masks_list::const_iterator r = regexps.begin(); + r != regexps.end(); + r++) { + static int ovec[30]; + int result = pcre_exec((pcre *)(*r).regexp, NULL, + str.c_str(), str.length(), 0, 0, ovec, 30); + if (result >= 0) { + match = ! (*r).exclude; + definite = true; + } + else if ((*r).exclude) { + if (! match) + match = ! definite; + } + else { + definite = true; + } + } + + if (by_exclusion) + *by_exclusion = match && ! definite && by_exclusion; + + return match; +} + +bool constraints_t::matches_date_range(const std::time_t date) const +{ + if (have_beginning && difftime(date, begin_date) < 0) + return false; + + if (have_ending && difftime(date, end_date) >= 0) + return false; + + if (have_date_mask) { + struct std::tm * then = std::gmtime(&date); + + if (date_mask.tm_mon != -1 && + date_mask.tm_mon != then->tm_mon) + return false; + + if (date_mask.tm_mday != -1 && + date_mask.tm_mday != then->tm_mday) + return false; + +#if 0 + // jww (2003-10-10): This causes only certain days of the week to + // print, even when it was not included in the mask. + if (date_mask.tm_wday != -1 && + date_mask.tm_wday != then->tm_wday) + return false; +#endif + + if (date_mask.tm_year != -1 && + date_mask.tm_year != then->tm_year) + return false; + } + + return true; +} + +bool constraints_t::operator ()(const transaction_t * xact) const +{ + if ((cleared_only && xact->entry->state != entry_t::CLEARED) || + (uncleared_only && xact->entry->state == entry_t::CLEARED) || + ! matches_date_range(xact->entry->date)) + return false; + + if (! payee_masks.empty() && + (! (matches(payee_masks, xact->entry->payee) + //|| matches(payee_masks, xact->entry->code)) + ))) + return false; + + if (real_only && xact->flags & TRANSACTION_VIRTUAL) + return false; + + if (! account_masks.empty() && + ! (matches(account_masks, std::string(*(xact->account))) + //|| matches(account_masks, (*i)->note) + )) + return false; + + return true; +} + +bool constraints_t::operator ()(const entry_t * entry) const +{ + if ((cleared_only && entry->state != entry_t::CLEARED) || + (uncleared_only && entry->state == entry_t::CLEARED) || + ! matches_date_range(entry->date)) + return false; + + if (! payee_masks.empty() && + (! (matches(payee_masks, entry->payee) + //|| matches(payee_masks, entry->code) + ))) + return false; + + if (! account_masks.empty()) { + bool match = false; + + for (transactions_list::const_iterator i = entry->transactions.begin(); + i != entry->transactions.end(); + i++) { + if (real_only && (*i)->flags & TRANSACTION_VIRTUAL) + continue; + + if (matches(account_masks, std::string(*((*i)->account))) + //|| matches(account_masks, (*i)->note) + ) { + match = true; + break; + } + } + + if (! match) + return false; + } + + return true; +} + +bool constraints_t::operator ()(const item_t * item) const +{ + if (predicate && ! predicate->compute(begin(), end(), item)) + return false; + + if (! matches_date_range(item->date)) + return false; + + if (! payee_masks.empty() && + ! (matches(payee_masks, item->payee))) + return false; + +#if 0 + if (! account_masks.empty()) { + bool match = false; + + for (amounts_map::const_iterator i = item->value.quantity.amounts.begin(); + i != item->value.quantity.amounts.end(); + i++) { + if (matches(account_masks, std::string(*((*i)->account))) + //|| matches(account_masks, (*i)->note) + ) { + match = true; + break; + } + } + + if (! match) + return false; + } +#endif + + return true; +} + +} // namespace ledger diff --git a/constraint.h b/constraint.h new file mode 100644 index 00000000..700a9e25 --- /dev/null +++ b/constraint.h @@ -0,0 +1,186 @@ +#ifndef _CONSTRAINT_H +#define _CONSTRAINT_H + +#include "ledger.h" +#include "item.h" + +template <typename ForwardIterator, typename ValueType, typename Constraint> +class constrained_iterator +{ + ForwardIterator iter, end; + const Constraint& constraint; + + constrained_iterator& operator=(const constrained_iterator&); + + public: + constrained_iterator(ForwardIterator begin, ForwardIterator _end, + const Constraint& _constraint) + : iter(begin), end(_end), constraint(_constraint) { + skip_nonmatching(); + } + + constrained_iterator(const constrained_iterator& other) + : iter(other.iter), end(other.end), constraint(other.constraint) {} + + constrained_iterator operator++() const { + ForwardIterator temp = iter; + temp++; + return constrained_iterator(temp, end); + } + + constrained_iterator& operator++(int) { + iter++; + skip_nonmatching(); + return *this; + } + + bool operator==(ForwardIterator other) const { + return iter == other; + } + bool operator!=(ForwardIterator other) const { + return ! (*this == other); + } + + ValueType operator*() const { + return *iter; + } + + void skip_nonmatching() { + bool failed; + do { + if (iter == end) return; + failed = false; + if (! constraint(*iter)) { + failed = true; + iter++; + } + } while (failed); + } +}; + +namespace ledger { + +class mask_t +{ + public: + bool exclude; + std::string pattern; + void * regexp; + + explicit mask_t(const std::string& pattern); + mask_t(const mask_t&); + + ~mask_t(); + + bool match(const std::string& str) const; +}; + +typedef std::list<mask_t> masks_list; + +bool matches(const masks_list& regexps, const std::string& str, + bool * by_exclusion = NULL); + + +struct item_t; + +enum periodicity_t { + PERIOD_NONE, + PERIOD_MONTHLY, + PERIOD_WEEKLY_SUN, + PERIOD_WEEKLY_MON +}; + +class constraints_t +{ + public: + bool real_only; + bool cleared_only; + bool uncleared_only; + + bool show_expanded; + bool show_related; + bool show_inverted; + bool show_subtotals; + bool show_empty; + + std::time_t begin_date; + bool have_beginning; + std::time_t end_date; + bool have_ending; + struct std::tm date_mask; + bool have_date_mask; + + masks_list payee_masks; + masks_list account_masks; + + periodicity_t period; + node_t * predicate; + node_t * sort_order; + + explicit constraints_t() { + real_only = false; + cleared_only = false; + uncleared_only = false; + + show_expanded = false; + show_related = false; + show_inverted = false; + show_subtotals = true; + show_empty = false; + + have_beginning = false; + have_ending = false; + have_date_mask = false; + + period = PERIOD_NONE; + predicate = NULL; + sort_order = NULL; + } + + ~constraints_t() { + if (predicate) delete predicate; + if (sort_order) delete sort_order; + } + + std::time_t begin() const { + return have_beginning ? begin_date : 0; + } + + std::time_t end() const { + return have_ending ? end_date : std::time(NULL); + } + + bool matches_date_range(const std::time_t date) const; + + bool operator ()(const transaction_t * xact) const; + bool operator ()(const entry_t * entry) const; + bool operator ()(const item_t * item) const; +}; + +typedef constrained_iterator<transactions_list::const_iterator, transaction_t *, + constraints_t> + constrained_transactions_list_const_iterator; + +typedef constrained_iterator<transactions_list::iterator, transaction_t *, + constraints_t> + constrained_transactions_list_iterator; + +typedef constrained_iterator<entries_list::const_iterator, entry_t *, + constraints_t> + constrained_entries_list_const_iterator; + +typedef constrained_iterator<entries_list::iterator, entry_t *, + constraints_t> + constrained_entries_list_iterator; + +typedef constrained_iterator<items_deque::const_iterator, item_t *, + constraints_t> + constrained_items_deque_const_iterator; + +typedef constrained_iterator<items_deque::iterator, item_t *, + constraints_t> + constrained_items_deque_iterator; + +} // namespace ledger + +#endif // _CONSTRAINT_H diff --git a/expr.cc b/expr.cc new file mode 100644 index 00000000..7318e903 --- /dev/null +++ b/expr.cc @@ -0,0 +1,602 @@ +#include "expr.h" + +namespace ledger { + +balance_t node_t::compute(const std::time_t begin, + const std::time_t end, + const item_t * item) const +{ + balance_t temp; + + switch (type) { + case CONSTANT_A: + temp = constant_a; + break; + + case CONSTANT_T: + temp = amount_t((unsigned int) constant_t); + break; + + case AMOUNT: + temp = item->value.quantity; + break; + case COST: + temp = item->value.cost; + break; + + case BALANCE: + temp = item->total.quantity - item->value.quantity; + break; + case COST_BALANCE: + temp = item->total.cost - item->value.cost; + break; + + case TOTAL: + temp = item->total.quantity; + break; + case COST_TOTAL: + temp = item->total.cost; + break; + + case DATE: + temp = amount_t((unsigned int) item->date); + break; + + case INDEX: + temp = amount_t(item->index + 1); + break; + + case BEGIN_DATE: + temp = amount_t((unsigned int) begin); + break; + + case END_DATE: + temp = amount_t((unsigned int) end); + break; + + case F_ARITH_MEAN: + assert(left); + temp = left->compute(begin, end, item); + temp /= amount_t(item->index + 1); + break; + + case F_NEG: + assert(left); + temp = left->compute(begin, end, item).negated(); + break; + + case F_ABS: + assert(left); + temp = abs(left->compute(begin, end, item)); + break; + + case F_REGEXP: + assert(mask); + temp = (item->account && + mask->match(item->account->fullname())) ? 1 : 0; + break; + + case F_VALUE: { + assert(left); + temp = left->compute(begin, end, item); + + std::time_t moment = -1; + if (right) { + switch (right->type) { + case DATE: moment = item->date; break; + case BEGIN_DATE: moment = begin; break; + case END_DATE: moment = end; break; + default: assert(0); break; + } + } + temp = temp.value(moment); + break; + } + + case O_NOT: + temp = left->compute(begin, end, item) ? 0 : 1; + break; + + case O_QUES: + temp = left->compute(begin, end, item); + if (temp) + temp = right->left->compute(begin, end, item); + else + temp = right->right->compute(begin, end, item); + break; + + case O_AND: + case O_OR: + case O_EQ: + case O_LT: + case O_LTE: + case O_GT: + case O_GTE: + case O_ADD: + case O_SUB: + case O_MUL: + case O_DIV: { + assert(left); + assert(right); + balance_t left_bal = left->compute(begin, end, item); + balance_t right_bal = right->compute(begin, end, item); + switch (type) { + case O_AND: temp = (left_bal && right_bal) ? 1 : 0; break; + case O_OR: temp = (left_bal || right_bal) ? 1 : 0; break; + case O_EQ: temp = (left_bal == right_bal) ? 1 : 0; break; + case O_LT: temp = (left_bal < right_bal) ? 1 : 0; break; + case O_LTE: temp = (left_bal <= right_bal) ? 1 : 0; break; + case O_GT: temp = (left_bal > right_bal) ? 1 : 0; break; + case O_GTE: temp = (left_bal >= right_bal) ? 1 : 0; break; + case O_ADD: temp = left_bal + right_bal; break; + case O_SUB: temp = left_bal - right_bal; break; + case O_MUL: temp = left_bal * right_bal; break; + case O_DIV: temp = left_bal / right_bal; break; + default: assert(0); break; + } + break; + } + + case LAST: + default: + assert(0); + break; + } + + return temp; +} + +void dump_tree(std::ostream& out, node_t * node) +{ + switch (node->type) { + case CONSTANT_A: out << "CONST[" << node->constant_a << "]"; break; + case CONSTANT_T: out << "DATE/TIME[" << node->constant_t << "]"; break; + case AMOUNT: out << "AMOUNT"; break; + case COST: out << "COST"; break; + case DATE: out << "DATE"; break; + case INDEX: out << "INDEX"; break; + case BALANCE: out << "BALANCE"; break; + case COST_BALANCE: out << "COST_BALANCE"; break; + case TOTAL: out << "TOTAL"; break; + case COST_TOTAL: out << "COST_TOTAL"; break; + case BEGIN_DATE: out << "BEGIN"; break; + case END_DATE: out << "END"; break; + + case F_ARITH_MEAN: + out << "MEAN("; + dump_tree(out, node->left); + out << ")"; + break; + + case F_NEG: + out << "ABS("; + dump_tree(out, node->left); + out << ")"; + break; + + case F_ABS: + out << "ABS("; + dump_tree(out, node->left); + out << ")"; + break; + + case F_REGEXP: + assert(node->mask); + out << "RE(" << node->mask->pattern << ")"; + break; + + case F_VALUE: + out << "VALUE("; + dump_tree(out, node->left); + if (node->right) { + out << ", "; + dump_tree(out, node->right); + } + out << ")"; + break; + + case O_NOT: + out << "!"; + dump_tree(out, node->left); + break; + + case O_QUES: + dump_tree(out, node->left); + out << "?"; + dump_tree(out, node->right->left); + out << ":"; + dump_tree(out, node->right->right); + break; + + case O_AND: + case O_OR: + case O_EQ: + case O_LT: + case O_LTE: + case O_GT: + case O_GTE: + case O_ADD: + case O_SUB: + case O_MUL: + case O_DIV: + out << "("; + dump_tree(out, node->left); + switch (node->type) { + case O_AND: out << " & "; break; + case O_OR: out << " | "; break; + case O_EQ: out << "="; break; + case O_LT: out << "<"; break; + case O_LTE: out << "<="; break; + case O_GT: out << ">"; break; + case O_GTE: out << ">="; break; + case O_ADD: out << "+"; break; + case O_SUB: out << "-"; break; + case O_MUL: out << "*"; break; + case O_DIV: out << "/"; break; + default: assert(0); break; + } + dump_tree(out, node->right); + out << ")"; + break; + + case LAST: + default: + assert(0); + break; + } +} + +node_t * parse_term(std::istream& in, ledger_t * ledger); + +inline node_t * parse_term(const char * p, ledger_t * ledger) { + std::istringstream stream(p); + return parse_term(stream, ledger); +} + +node_t * parse_term(std::istream& in, ledger_t * ledger) +{ + node_t * node = NULL; + + char c = in.peek(); + if (std::isdigit(c) || c == '.' || c == '{') { + std::string ident; + + if (c == '{') { + in.get(c); + c = in.peek(); + while (! in.eof() && c != '}') { + in.get(c); + ident += c; + c = in.peek(); + } + if (c == '}') + in.get(c); + else + ident = "0"; + } else { + while (! in.eof() && std::isdigit(c) || c == '.') { + in.get(c); + ident += c; + c = in.peek(); + } + } + + if (! ident.empty()) { + node = new node_t(CONSTANT_A); + node->constant_a.parse(ident, ledger); + } + return node; + } + + in.get(c); + switch (c) { + // Basic terms + case 'a': node = new node_t(AMOUNT); break; + case 'c': node = new node_t(COST); break; + case 'd': node = new node_t(DATE); break; + case 'b': node = new node_t(BEGIN_DATE); break; + case 'e': node = new node_t(END_DATE); break; + case 'i': node = new node_t(INDEX); break; + case 'B': node = new node_t(BALANCE); break; + case 'T': node = new node_t(TOTAL); break; + case 'C': node = new node_t(COST_TOTAL); break; + + // Compound terms + case 'v': node = parse_expr("P(a,d)", ledger); break; + case 'V': node = parse_term("P(T,d)", ledger); break; + case 'g': node = parse_expr("v-c", ledger); break; + case 'G': node = parse_expr("V-C", ledger); break; + case 'o': node = parse_expr("d-b", ledger); break; + case 'w': node = parse_expr("e-d", ledger); break; + + // Functions + case '-': + node = new node_t(F_NEG); + node->left = parse_term(in, ledger); + break; + + case 'A': // absolute value ("positive") + node = new node_t(F_ABS); + node->left = parse_term(in, ledger); + break; + + case 'M': + node = new node_t(F_ARITH_MEAN); + node->left = parse_term(in, ledger); + break; + + case 'D': { + node = new node_t(O_SUB); + node->left = parse_term("a", ledger); + node->right = parse_term(in, ledger); + break; + } + + case 'P': + node = new node_t(F_VALUE); + if (in.peek() == '(') { + in.get(c); + node->left = parse_expr(in, ledger); + if (in.peek() == ',') { + in.get(c); + node->right = parse_expr(in, ledger); + } + if (in.peek() == ')') + in.get(c); + } else { + node->left = parse_term(in, ledger); + } + break; + + // Other + case '/': { + std::string ident; + + c = in.peek(); + while (! in.eof() && c != '/') { + in.get(c); + if (c == '\\') + in.get(c); + ident += c; + c = in.peek(); + } + if (c == '/') { + in.get(c); + node = new node_t(F_REGEXP); + node->mask = new mask_t(ident); + } else { + assert(0); + } + break; + } + + case '(': + node = parse_expr(in, ledger); + if (in.peek() == ')') + in.get(c); + else + assert(0); + break; + + case '[': { + std::string ident; + + c = in.peek(); + while (! in.eof() && c != ']') { + in.get(c); + ident += c; + c = in.peek(); + } + if (c == ']') { + in.get(c); + node = new node_t(CONSTANT_T); + if (! parse_date(ident.c_str(), &node->constant_t)) + assert(0); + } else { + assert(0); + } + break; + } + + default: + in.unget(); + break; + } + + return node; +} + +node_t * parse_mul_expr(std::istream& in, ledger_t * ledger) +{ + node_t * node = NULL; + + node = parse_term(in, ledger); + + if (node && ! in.eof()) { + char c = in.peek(); + while (c == '*' || c == '/') { + in.get(c); + switch (c) { + case '*': { + node_t * prev = node; + node = new node_t(O_MUL); + node->left = prev; + node->right = parse_term(in, ledger); + break; + } + + case '/': { + node_t * prev = node; + node = new node_t(O_DIV); + node->left = prev; + node->right = parse_term(in, ledger); + break; + } + } + c = in.peek(); + } + } + + return node; +} + +node_t * parse_add_expr(std::istream& in, ledger_t * ledger) +{ + node_t * node = NULL; + + node = parse_mul_expr(in, ledger); + + if (node && ! in.eof()) { + char c = in.peek(); + while (c == '+' || c == '-') { + in.get(c); + switch (c) { + case '+': { + node_t * prev = node; + node = new node_t(O_ADD); + node->left = prev; + node->right = parse_mul_expr(in, ledger); + break; + } + + case '-': { + node_t * prev = node; + node = new node_t(O_SUB); + node->left = prev; + node->right = parse_mul_expr(in, ledger); + break; + } + } + c = in.peek(); + } + } + + return node; +} + +node_t * parse_logic_expr(std::istream& in, ledger_t * ledger) +{ + node_t * node = NULL; + + if (in.peek() == '!') { + char c; + in.get(c); + node = new node_t(O_NOT); + node->left = parse_logic_expr(in, ledger); + return node; + } + + node = parse_add_expr(in, ledger); + + if (node && ! in.eof()) { + char c = in.peek(); + if (c == '=' || c == '<' || c == '>') { + in.get(c); + switch (c) { + case '=': { + node_t * prev = node; + node = new node_t(O_EQ); + node->left = prev; + node->right = parse_add_expr(in, ledger); + break; + } + + case '<': { + node_t * prev = node; + node = new node_t(O_LT); + if (in.peek() == '=') { + in.get(c); + node->type = O_LTE; + } + node->left = prev; + node->right = parse_add_expr(in, ledger); + break; + } + + case '>': { + node_t * prev = node; + node = new node_t(O_GT); + if (in.peek() == '=') { + in.get(c); + node->type = O_GTE; + } + node->left = prev; + node->right = parse_add_expr(in, ledger); + break; + } + + default: + if (! in.eof()) + assert(0); + break; + } + } + } + + return node; +} + +node_t * parse_expr(std::istream& in, ledger_t * ledger) +{ + node_t * node = NULL; + + node = parse_logic_expr(in, ledger); + + if (node && ! in.eof()) { + char c = in.peek(); + while (c == '&' || c == '|' || c == '?') { + in.get(c); + switch (c) { + case '&': { + node_t * prev = node; + node = new node_t(O_AND); + node->left = prev; + node->right = parse_logic_expr(in, ledger); + break; + } + + case '|': { + node_t * prev = node; + node = new node_t(O_OR); + node->left = prev; + node->right = parse_logic_expr(in, ledger); + break; + } + + case '?': { + node_t * prev = node; + node = new node_t(O_QUES); + node->left = prev; + node_t * choices = new node_t(O_COL); + node->right = choices; + choices->left = parse_logic_expr(in, ledger); + c = in.peek(); + assert(c == ':'); + in.get(c); + choices->right = parse_logic_expr(in, ledger); + break; + } + + default: + if (! in.eof()) + assert(0); + break; + } + c = in.peek(); + } + } + + return node; +} + +} // namespace ledger + +#ifdef TEST + +int main(int argc, char *argv[]) +{ + ledger::dump_tree(std::cout, ledger::parse_expr(argv[1], NULL)); + std::cout << std::endl; +} + +#endif @@ -0,0 +1,112 @@ +#ifndef _REPORT_H +#define _REPORT_H + +#include "ledger.h" +#include "constraint.h" +#include "balance.h" + +namespace ledger { + +enum kind_t { + // Constants + CONSTANT_A, + CONSTANT_T, + + // Item details + AMOUNT, + COST, + DATE, + INDEX, + + // Item totals + BALANCE, + COST_BALANCE, + TOTAL, + COST_TOTAL, + + // Constraint details + BEGIN_DATE, + END_DATE, + + // Functions + F_ARITH_MEAN, + F_VALUE, + F_NEG, + F_ABS, + F_REGEXP, + + // Binary operators + O_ADD, + O_SUB, + O_MUL, + O_DIV, + O_EQ, + O_LT, + O_LTE, + O_GT, + O_GTE, + O_NOT, + O_AND, + O_OR, + O_QUES, + O_COL, + + LAST +}; + +struct node_t +{ + kind_t type; + node_t * left; + node_t * right; + + amount_t constant_a; + std::time_t constant_t; + mask_t * mask; + + node_t(const kind_t _type) + : type(_type), left(NULL), right(NULL) {} + + ~node_t() { + if (mask) delete mask; + if (left) delete left; + if (right) delete right; + } + + balance_t compute(const item_t * item, + const std::time_t begin = -1, + const std::time_t end = -1) const; + + balance_t compute(const item_t * item, + const constraint_t& constraints) const { + return compute(item, constraints.begin(), constraints.end()); + } +}; + +node_t * parse_expr(std::istream& in, ledger_t * ledger); + +inline node_t * parse_expr(const char * p, ledger_t * ledger) { + std::istringstream stream(p); + return parse_expr(stream, ledger); +} + +inline node_t * parse_expr(const std::string& str, ledger_t * ledger) { + return parse_expr(str.c_str(), ledger); +} + +inline node_t * find_node(node_t * node, kind_t type) { + node_t * result = NULL; + if (node->type == type) + result = node; + if (! result && node->left) + result = find_node(node->left, type); + if (! result && node->right) + result = find_node(node->right, type); + return result; +} + +void dump_tree(std::ostream& out, node_t * node); + +} // namespace report + +#endif // _REPORT_H diff --git a/format.cc b/format.cc new file mode 100644 index 00000000..a68852af --- /dev/null +++ b/format.cc @@ -0,0 +1,195 @@ +#include "format.h" + +namespace ledger { + +std::string truncated(const std::string& str, unsigned int width) +{ + char buf[256]; + std::memset(buf, '\0', 255); + assert(width < 256); + std::strncpy(buf, str.c_str(), str.length()); + if (buf[width]) + std::strcpy(&buf[width - 2], ".."); + return buf; +} + +std::string maximal_account_name(const item_t * item, + const item_t * parent) +{ + std::string name = item->account->name; + for (const item_t * i = item->parent; + i && i->account && i != parent; + i = i->parent) + name = i->account->name + ":" + name; + return name; +} + +std::string format_string(const item_t * item, const format_t& format, + const item_t * displayed_parent) +{ + std::string result; + + for (const char * p = format.format_string.c_str(); *p; p++) { + if (*p == '%') { + bool leftalign = false; + bool ignore = false; + int width = 0; + int strict_width = 0; + + ++p; + if (*p == '?') { + ignore = false; //subsequent_line; + ++p; + } + + if (*p == '-') { + leftalign = true; + ++p; + } + + std::string num; + while (*p && std::isdigit(*p)) + num += *p++; + if (! num.empty()) + width = std::atol(num.c_str()); + + if (*p == '.') { + ++p; + num = ""; + while (*p && std::isdigit(*p)) + num += *p++; + if (! num.empty()) { + strict_width = std::atol(num.c_str()); + if (width == 0) + width = strict_width; + } + } + + std::ostringstream out; + + if (leftalign) + out << std::left; + else + out << std::right; + + if (width > 0) + out.width(width); + + if (ignore) { + out << " "; + result += out.str(); + continue; + } + + switch (*p) { + case '%': + out << "%"; + break; + + case '(': { + ++p; + num = ""; + while (*p && *p != ')') + num += *p++; + assert(*p == ')'); + + node_t * style = parse_expr(num, NULL); + balance_t value = style->compute(format.begin(), format.end(), item); + value.write(out, width, strict_width > 0 ? strict_width : width); + break; + } + + case '[': { + ++p; + num = ""; + while (*p && *p != ']') + num += *p++; + assert(*p == ']'); + + if (item->date != -1) { + char buf[256]; + std::strftime(buf, 255, num.c_str(), std::gmtime(&item->date)); + out << (strict_width == 0 ? buf : truncated(buf, strict_width)); + } else { + out << " "; + } + break; + } + + case 'd': { + if (item->date != -1) { + char buf[32]; + std::strftime(buf, 31, "%Y/%m/%d", std::gmtime(&item->date)); + out << (strict_width == 0 ? buf : truncated(buf, strict_width)); + } else { + out << " "; + } + break; + } + + case 'p': + out << (strict_width == 0 ? + item->payee : truncated(item->payee, strict_width)); + break; + + case 'n': + if (item->account) { + std::string name = maximal_account_name(item, displayed_parent); + out << (strict_width == 0 ? name : truncated(name, strict_width)); + } else { + out << " "; + } + break; + + case 'N': + if (item->account) + out << (strict_width == 0 ? + item->account->fullname() : + truncated(item->account->fullname(), strict_width)); + else + out << " "; + break; + + case 't': + if (format.value_style) { + balance_t value = format.compute_value(item); + value.write(out, width, strict_width > 0 ? strict_width : width); + } + break; + + case 'T': + if (format.total_style) { + balance_t value = format.compute_total(item); + value.write(out, width, strict_width > 0 ? strict_width : width); + } + break; + + case '_': { + int depth = 0; + for (const item_t * i = item; i->parent; i = i->parent) + depth++; + + for (const item_t * i = item->parent; + i && i->account && i != displayed_parent; + i = i->parent) + depth--; + + while (--depth >= 0) { + if (width > 0 || strict_width > 0) + out.width(width > strict_width ? width : strict_width); + out << " "; + } + break; + } + } + + result += out.str(); + } else { + result += *p; + } + } + + return result; +} + +} // namespace ledger diff --git a/format.h b/format.h new file mode 100644 index 00000000..3e66dc7b --- /dev/null +++ b/format.h @@ -0,0 +1,51 @@ +#ifndef _REPORT_H +#define _REPORT_H + +#include "ledger.h" +#include "constraint.h" +#include "balance.h" + +namespace ledger { + +std::string truncated(const std::string& str, unsigned int width); +std::string maximal_account_name(const item_t * item, const item_t * parent); + +struct format_t +{ + constraints_t constraints; + + std::string format_string; + node_t * value_style; + node_t * total_style; + + format_t() { + value_style = NULL; + total_style = NULL; + } + + ~format_t() { + if (value_style) delete value_style; + if (total_style) delete total_style; + } + + balance_t compute_value(const item_t * item) const { + if (value_style) + return value_style->compute(begin(), end(), item); + else + return balance_t(); + } + + balance_t compute_total(const item_t * item) const { + if (total_style) + return total_style->compute(begin(), end(), item); + else + return balance_t(); + } + + std::string report_line(const item_t * item, + const item_t * displayed_parent = NULL); +}; + +} // namespace ledger + +#endif // _REPORT_H @@ -9,16 +9,25 @@ extern "C" { namespace ledger { -static account * curr_account; -static std::string curr_account_id; -static entry * curr_entry; -static commodity * entry_comm; -static commodity * curr_comm; -static amount * curr_value; -static std::string curr_quant; -static XML_Parser current_parser; -static accounts_map accounts_by_id; -static bool do_compute; +typedef std::map<const std::string, account_t *> accounts_map; +typedef std::pair<const std::string, account_t *> accounts_pair; + +typedef std::map<account_t *, commodity_t *> account_comm_map; +typedef std::pair<account_t *, commodity_t *> account_comm_pair; + +static ledger_t * curr_ledger; +static account_t * curr_account; +static commodity_t * curr_account_comm; +static std::string curr_account_id; +static entry_t * curr_entry; +static commodity_t * entry_comm; +static commodity_t * curr_comm; +static amount_t curr_value; +static amount_t curr_quant; +static XML_Parser current_parser; +static accounts_map accounts_by_id; +static account_comm_map account_comms; +static unsigned int count; static enum { NO_ACTION, @@ -44,7 +53,7 @@ static void startElement(void *userData, const char *name, const char **atts) { if (std::strcmp(name, "gnc:account") == 0) { assert(! curr_account); - curr_account = new account; + curr_account = new account_t(curr_account); } else if (std::strcmp(name, "act:name") == 0) action = ACCOUNT_NAME; @@ -54,7 +63,7 @@ static void startElement(void *userData, const char *name, const char **atts) action = ACCOUNT_PARENT; else if (std::strcmp(name, "gnc:commodity") == 0) { assert(! curr_comm); - curr_comm = new commodity; + curr_comm = new commodity_t(""); } else if (std::strcmp(name, "cmdty:id") == 0) action = COMM_SYM; @@ -64,7 +73,7 @@ static void startElement(void *userData, const char *name, const char **atts) action = COMM_PREC; else if (std::strcmp(name, "gnc:transaction") == 0) { assert(! curr_entry); - curr_entry = new entry(main_ledger); + curr_entry = new entry_t; } else if (std::strcmp(name, "trn:num") == 0) action = ENTRY_NUM; @@ -76,7 +85,8 @@ static void startElement(void *userData, const char *name, const char **atts) action = ENTRY_DESC; else if (std::strcmp(name, "trn:split") == 0) { assert(curr_entry); - curr_entry->xacts.push_back(new transaction()); + curr_entry->add_transaction(new transaction_t(curr_entry, curr_account, + amount_t())); } else if (std::strcmp(name, "split:reconciled-state") == 0) action = XACT_STATE; @@ -92,32 +102,47 @@ static void startElement(void *userData, const char *name, const char **atts) action = XACT_NOTE; } - static void endElement(void *userData, const char *name) { if (std::strcmp(name, "gnc:account") == 0) { assert(curr_account); if (! curr_account->parent) - main_ledger->accounts.insert(accounts_map_pair(curr_account->name, - curr_account)); - accounts_by_id.insert(accounts_map_pair(curr_account_id, curr_account)); + curr_ledger->add_account(curr_account); + accounts_by_id.insert(accounts_pair(curr_account_id, curr_account)); curr_account = NULL; } else if (std::strcmp(name, "gnc:commodity") == 0) { assert(curr_comm); - main_ledger->commodities.insert(commodities_map_pair(curr_comm->symbol, - curr_comm)); + curr_ledger->add_commodity(curr_comm); curr_comm = NULL; } else if (std::strcmp(name, "gnc:transaction") == 0) { assert(curr_entry); - assert(curr_entry->validate()); - main_ledger->entries.push_back(curr_entry); + if (! curr_ledger->add_entry(curr_entry)) + assert(0); curr_entry = NULL; } action = NO_ACTION; } + +static amount_t convert_number(const std::string& number) +{ + const char * num = number.c_str(); + + if (char * p = std::strchr(num, '/')) { + std::string numer_str(num, p - num); + std::string denom_str(p + 1); + + amount_t amt(numer_str); + amount_t den(denom_str); + + return amt / den; + } else { + return amount_t(number); + } +} + static void dataHandler(void *userData, const char *s, int len) { switch (action) { @@ -130,11 +155,10 @@ static void dataHandler(void *userData, const char *s, int len) break; case ACCOUNT_PARENT: { - accounts_map_iterator i = accounts_by_id.find(std::string(s, len)); + accounts_map::iterator i = accounts_by_id.find(std::string(s, len)); assert(i != accounts_by_id.end()); curr_account->parent = (*i).second; - (*i).second->children.insert(accounts_map_pair(curr_account->name, - curr_account)); + (*i).second->add_account(curr_account); break; } @@ -142,9 +166,9 @@ static void dataHandler(void *userData, const char *s, int len) if (curr_comm) curr_comm->symbol = std::string(s, len); else if (curr_account) - curr_account->comm = main_ledger->commodities[std::string(s, len)]; + curr_account_comm = curr_ledger->find_commodity(std::string(s, len)); else if (curr_entry) - entry_comm = main_ledger->commodities[std::string(s, len)]; + entry_comm = curr_ledger->find_commodity(std::string(s, len)); break; case COMM_NAME: @@ -167,57 +191,58 @@ static void dataHandler(void *userData, const char *s, int len) } case ENTRY_DESC: - curr_entry->desc = std::string(s, len); + curr_entry->payee = std::string(s, len); break; case XACT_STATE: - curr_entry->cleared = (*s == 'y' || *s == 'c'); + if (*s == 'y') + curr_entry->state = entry_t::PENDING; + else + curr_entry->state = entry_t::CLEARED; break; - case XACT_VALUE: { + case XACT_VALUE: assert(entry_comm); - std::string value = std::string(s, len) + " " + entry_comm->symbol; - curr_value = create_amount(value.c_str()); + curr_value = convert_number(std::string(s, len) + " " + + entry_comm->symbol); break; - } case XACT_QUANTITY: - curr_quant = std::string(s, len); + curr_quant = convert_number(std::string(s, len)); break; case XACT_ACCOUNT: { - accounts_map_iterator i = accounts_by_id.find(std::string(s, len)); + accounts_map::iterator i = accounts_by_id.find(std::string(s, len)); if (i == accounts_by_id.end()) { std::cerr << "Could not find account " << std::string(s, len) << std::endl; std::exit(1); } - transaction * xact = curr_entry->xacts.back(); - xact->acct = (*i).second; + transaction_t * xact = curr_entry->transactions.back(); + xact->account = (*i).second; - std::string value = curr_quant + " " + xact->acct->comm->symbol; - - if (curr_value->commdty() == xact->acct->comm) { - // assert: value must be equal to curr_value. - delete curr_value; - curr_value = NULL; + account_comm_map::iterator ac = account_comms.find(xact->account); + if (ac == account_comms.end()) { + std::cerr << "Could not find account " << *(xact->account) + << std::endl; + std::exit(1); } + commodity_t * default_commodity = (*ac).second; - xact->cost = create_amount(value.c_str(), curr_value); + curr_quant.commodity = default_commodity; + amount_t value = curr_quant.round(default_commodity->precision); - if (curr_value) { - delete curr_value; - curr_value = NULL; - } + if (curr_value.commodity == default_commodity) + curr_value = value; - if (do_compute) - xact->acct->balance.credit(xact->cost); + xact->amount = value; + xact->cost = curr_value; break; } case XACT_NOTE: - curr_entry->xacts.back()->note = std::string(s, len); + curr_entry->transactions.back()->note = std::string(s, len); break; case NO_ACTION: @@ -231,25 +256,23 @@ static void dataHandler(void *userData, const char *s, int len) } } -book * parse_gnucash(std::istream& in, bool compute_balances) +int parse_gnucash(std::istream& in, ledger_t * ledger, account_t * master) { char buf[BUFSIZ]; - book * ledger = new book; - - main_ledger = ledger; - do_compute = compute_balances; + count = 0; action = NO_ACTION; + curr_ledger = ledger; curr_account = NULL; curr_entry = NULL; - curr_value = NULL; curr_comm = NULL; entry_comm = NULL; // GnuCash uses the USD commodity without defining it, which really - // means to use $. - commodity * usd = new commodity("$", true, false, true, false, 2); - main_ledger->commodities.insert(commodities_map_pair("USD", usd)); + // means $. + commodity_t * usd = new commodity_t("$", 2, COMMODITY_STYLE_THOUSANDS); + ledger->add_commodity(usd); + ledger->add_commodity(usd, "USD"); XML_Parser parser = XML_ParserCreate(NULL); current_parser = parser; @@ -271,11 +294,8 @@ book * parse_gnucash(std::istream& in, bool compute_balances) accounts_by_id.clear(); curr_account_id.clear(); - curr_quant.clear(); - - main_ledger->commodities.erase("USD"); - return ledger; + return count; } } // namespace ledger diff --git a/item.cc b/item.cc new file mode 100644 index 00000000..7e45ab07 --- /dev/null +++ b/item.cc @@ -0,0 +1,224 @@ +#include "item.h" + +namespace ledger { + +// jww (2004-07-21): If format.show_empty is set, then include all +// subaccounts, empty balanced or no + +item_t * walk_accounts(const account_t * account, + const constraints_t& constraints, + const bool compute_subtotals) +{ + item_t * item = new item_t; + item->account = account; + item->date = end_date(constraints); + + for (constrained_transactions_list_const_iterator + i(account->transactions.begin(), + account->transactions.end(), constraints); + i != account->transactions.end(); + i++) { + item->value += *(*i); + if (compute_subtotals) + item->total += *(*i); + } + + for (accounts_map::const_iterator i = account->accounts.begin(); + i != account->accounts.end(); + i++) { + item_t * subitem = walk_accounts((*i).second, constraints, + compute_subtotals); + subitem->parent = item; + + if (compute_subtotals) + item->total += subitem->total; + + if (compute_subtotals ? subitem->total : subitem->value) + item->subitems.push_back(subitem); + } + + return item; +} + +static inline void sum_items(const item_t * top, + item_t * item, + const bool compute_subtotals) +{ + if (top->account == item->account) { + item->value += top->value; + if (compute_subtotals) + item->total += top->value; + } + + for (items_deque::const_iterator i = top->subitems.begin(); + i != top->subitems.end(); + i++) + sum_items(*i, item, compute_subtotals); +} + +item_t * walk_items(const item_t * top, + const account_t * account, + const constraints_t& constraints, + const bool compute_subtotals) +{ + item_t * item = new item_t; + item->account = account; + + sum_items(top, item, compute_subtotals); + + for (accounts_map::const_iterator i = account->accounts.begin(); + i != account->accounts.end(); + i++) { + item_t * subitem = walk_items(top, (*i).second, constraints, + compute_subtotals); + subitem->parent = item; + + if (compute_subtotals) + item->total += subitem->total; + + if (compute_subtotals ? subitem->total : subitem->value) + item->subitems.push_back(subitem); + } + + return item; +} + +item_t * walk_entries(entries_list::const_iterator begin, + entries_list::const_iterator end, + const constraints_t& constraints, + const format_t& format) +{ +#if 0 + int last_mon = -1; +#endif + unsigned int count = 0; + item_t * result = NULL; + + for (constrained_entries_list_const_iterator i(begin, end, constraints); + i != end; + i++) { + item_t * item = NULL; + + for (constrained_transactions_list_const_iterator + j((*i)->transactions.begin(), (*i)->transactions.end(), + constraints); + j != (*i)->transactions.end(); + j++) { + assert(*i == (*j)->entry); + + if (! item) { + item = new item_t; + item->index = count++; + item->date = (*i)->date; + item->payee = (*i)->payee; + } + + if (! format.show_inverted) { + item_t * subitem = new item_t; + subitem->parent = item; + subitem->date = item->date; + subitem->account = (*j)->account; + subitem->value = *(*j); + item->subitems.push_back(subitem); + } + + if (format.show_related) + for (transactions_list::iterator k = (*i)->transactions.begin(); + k != (*i)->transactions.end(); + k++) + if (*k != *j && ! ((*k)->flags & TRANSACTION_VIRTUAL)) { + item_t * subitem = new item_t; + subitem->parent = item; + subitem->date = item->date; + subitem->account = (*k)->account; + subitem->value = *(*k); + if (format.show_inverted) + subitem->value.negate(); + item->subitems.push_back(subitem); + } + +#if 0 + // If we are collecting monthly totals, then add them if the + // month of this entry is different from the month of previous + // entries. + + if (format.period == PERIOD_MONTHLY) { + int entry_mon = std::gmtime(&(*i)->date)->tm_mon; + + if (last_mon != -1 && entry_mon != last_mon && + line_balances.size() > 0) { + if (last_date == 0) + last_date = (*i)->date; + + if (reg) { + char buf[32]; + std::strftime(buf, 31, "%B", std::gmtime(&last_date)); + + reg->lines.push_back(register_line_t(last_date, buf)); + reg->lines.back().compute_items(line_balances, total, count); + } else { + count++; + } + + line_balances.clear(); + } + + last_mon = entry_mon; + last_date = (*i)->date; + } +#endif + } + + if (item) { + if (! result) + result = new item_t; + item->parent = result; + result->subitems.push_back(item); + } + } + + return result; + +#if 0 + // Wrap up any left over balance list information. + + if (line_balances.size() > 0) { + assert(format.period == PERIOD_MONTHLY); + assert(last_date != 0); + + if (reg) { + char buf[32]; + std::strftime(buf, 31, "%B", std::gmtime(&last_date)); + + reg->lines.push_back(register_line_t(last_date, buf)); + reg->lines.back().compute_items(line_balances, total, count); + } else { + count++; + } + + //line_balances.clear(); + } + + return count; +#endif +} + +struct cmp_items { + const node_t * sort_order; + + cmp_items(const node_t * _sort_order) : sort_order(_sort_order) {} + + bool operator()(const item_t * left, const item_t * right) const { + assert(left); + assert(right); + assert(sort_order); + return sort_order->compute(left) < sort_order->compute(right); + } +}; + +void item_t::sort(const node_t * sort_order) +{ + std::sort(subitems.begin(), subitems.end(), cmp_items(sort_order)); +} + +} // namespace ledger @@ -0,0 +1,57 @@ +#ifndef _REPORT_H +#define _REPORT_H + +#include "ledger.h" +#include "balance.h" + +#include <deque> + +namespace ledger { + +struct node_t; +struct item_t; +typedef std::deque<item_t *> items_deque; + +struct item_t +{ + struct item_t * parent; + unsigned int index; + std::time_t date; + std::string payee; + const account_t * account; + balance_pair_t value; + balance_pair_t total; + + items_deque subitems; + + item_t() : parent(NULL), index(0), date(-1), account(NULL) {} + + ~item_t() { + for (items_deque::iterator i = subitems.begin(); + i != subitems.end(); + i++) + delete *i; + } + + void sort(const node_t * sort_order); +}; + +class constraints_t; + +item_t * walk_accounts(const account_t * account, + const constraints_t& constraints, + const bool compute_subtotals); + +item_t * walk_items(const item_t * top, + const account_t * account, + const constraints_t& constraints, + const bool compute_subtotals); + +item_t * walk_entries(entries_list::const_iterator begin, + entries_list::const_iterator end, + const constraints_t& constraints, + const format_t& format); + +} // namespace report + +#endif // _REPORT_H @@ -1,674 +1,104 @@ #include "ledger.h" +#include "report.h" +#include "textual.h" +#include "binary.h" #include <fstream> -#include <unistd.h> +#include <deque> namespace ledger { -book * main_ledger; +const std::string version = "2.0b"; -extern int linenum; - -commodity::~commodity() +ledger_t::~ledger_t() { - if (conversion) - delete conversion; + delete master; - for (price_map::iterator i = history.begin(); - i != history.end(); + for (commodities_map::iterator i = commodities.begin(); + i != commodities.end(); i++) delete (*i).second; -} - -void commodity::set_price(amount * price, std::time_t * when) -{ - assert(price); - if (when) - history.insert(price_map_pair(*when, price)); - else - conversion = price; -} -amount * commodity::price(std::time_t * when, - bool use_history, bool download) const -{ - if (conversion || ! when || ! use_history) - return conversion; - - std::time_t age; - amount * price = NULL; - - for (price_map::reverse_iterator i = history.rbegin(); - i != history.rend(); + // Don't bother unhooking each entry's transactions from the + // accounts they refer to, because all accounts are about to + // be deleted. + for (entries_list::iterator i = entries.begin(); + i != entries.end(); i++) - if (std::difftime(*when, (*i).first) >= 0) { - age = (*i).first; - price = (*i).second; - break; - } - - extern long pricing_leeway; - time_t now = time(NULL); // the time of the query - - if (download && ! sought && - std::difftime(now, *when) < pricing_leeway && - (! price || std::difftime(*when, age) > pricing_leeway)) { - using namespace std; - - // Only consult the Internet once for any commodity - sought = true; - - char buf[256]; - buf[0] = '\0'; - - if (FILE * fp = popen((string("getquote ") + symbol).c_str(), "r")) { - if (feof(fp) || ! fgets(buf, 255, fp)) { - fclose(fp); - return price; - } - fclose(fp); - } - - if (buf[0]) { - char * p = strchr(buf, '\n'); - if (p) *p = '\0'; - - price = create_amount(buf); - const_cast<commodity *>(this)->set_price(price, &now); - - extern string price_db; - if (! price_db.empty()) { - char buf[128]; - strftime(buf, 127, "%Y/%m/%d %H:%M:%S", localtime(&now)); - ofstream database(price_db.c_str(), ios_base::out | ios_base::app); - database << "P " << buf << " " << symbol << " " - << price->as_str() << endl; - } - } - } - - return price; -} - -const std::string transaction::acct_as_str() const -{ - char * begin = NULL; - char * end = NULL; - - if (is_virtual) { - if (must_balance) { - begin = "["; - end = "]"; - } else { - begin = "("; - end = ")"; - } - } - - if (begin) - return std::string(begin) + acct->as_str() + end; - else - return acct->as_str(); -} - -void transaction::print(std::ostream& out, bool display_quantity, - bool display_price) const -{ - out.width(30); - out << std::left << acct_as_str(); - - if (cost && display_quantity) { - out << " "; - out.width(12); - - std::string value = cost->as_str(true); - if (! display_price) { - int index = value.find('@'); - if (index != -1) - value = std::string(value, 0, index - 1); - } - out << std::right << value; - } - - if (! note.empty()) - out << " ; " << note; - - out << std::endl; + delete *i; } -void entry::print(std::ostream& out, bool shortcut) const +commodity_t * ledger_t::find_commodity(const std::string& symbol, + bool auto_create) { - char buf[32]; - std::strftime(buf, 31, "%Y/%m/%d ", std::localtime(&date)); - out << buf; - - if (cleared) - out << "* "; - if (! code.empty()) - out << '(' << code << ") "; - if (! desc.empty()) - out << desc; - - out << std::endl; - - commodity * comm = NULL; - int size = 0; - - for (std::list<transaction *>::const_iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if ((*x)->is_virtual && ! (*x)->must_balance) - continue; - - if (! comm) - comm = (*x)->cost->commdty(); - else if (comm != (*x)->cost->commdty()) - shortcut = false; - - size++; - } - - if (shortcut && size != 2) - shortcut = false; - - for (std::list<transaction *>::const_iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if ((*x)->is_virtual && ! (*x)->specified) - continue; - - out << " "; + commodities_map::const_iterator i = commodities.find(symbol); + if (i != commodities.end()) + return (*i).second; - (*x)->print(out, (! shortcut || x == xacts.begin() || - ((*x)->is_virtual && ! (*x)->must_balance)), - size != 2); + if (auto_create) { + commodity_t * commodity = new commodity_t(symbol); + add_commodity(commodity); + return commodity; } - out << std::endl; -} - -bool entry::validate(bool show_unaccounted) const -{ - totals balance; - - for (std::list<transaction *>::const_iterator x = xacts.begin(); - x != xacts.end(); - x++) - if ((*x)->cost && (*x)->must_balance) { - amount * value = (*x)->cost->value(); - balance.credit(value); - delete value; - } - - if (show_unaccounted && ! balance.is_zero()) { - std::cerr << "Unaccounted-for balances are:" << std::endl; - balance.print(std::cerr, 20); - std::cerr << std::endl << std::endl; - } - return balance.is_zero(); // must balance to 0.0 + return NULL; } -bool entry::finalize(bool do_compute) +bool ledger_t::add_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. - - totals balance; - - for (std::list<transaction *>::iterator x = xacts.begin(); - x != xacts.end(); - x++) - if ((*x)->cost && ! (*x)->is_virtual) { - amount * value = (*x)->cost->value(); - balance.credit(value); - delete value; - } - - // 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 (std::list<transaction *>::iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if ((*x)->is_virtual || (*x)->cost->has_price()) - continue; - - for (totals::iterator i = balance.amounts.begin(); - i != balance.amounts.end(); - i++) - if ((*i).second->commdty() != (*x)->cost->commdty()) { - (*x)->cost->set_value((*i).second); - assert((*x)->cost->has_price()); - (*x)->cost->commdty()->set_price((*x)->cost->per_item_price(), - &date); - 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 (std::list<transaction *>::iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if ((*x)->is_virtual || (*x)->cost) - continue; - - if (! empty_allowed || balance.amounts.empty() || - balance.amounts.size() != 1) { - std::cerr << "Error, line " << linenum - << ": Transaction entry is lacking an amount." - << std::endl; - 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. - - totals::iterator i = balance.amounts.begin(); - (*x)->cost = (*i).second->value(); - (*x)->cost->negate(); + entries.push_back(entry); - if (do_compute) - (*x)->acct->balance.credit((*x)->cost); - } - - // If automated transactions are being used, walk through the - // current transaction lines and create new transactions for all - // that match. - - for (book::virtual_map_iterator m = ledger->virtual_mapping.begin(); - m != ledger->virtual_mapping.end(); - m++) { - std::list<transaction *> new_xacts; - - for (std::list<transaction *>::iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if ((*x)->is_virtual || - ! ledger::matches(*((*m).first), (*x)->acct->as_str())) - continue; - - for (std::list<transaction *>::iterator i = (*m).second->begin(); - i != (*m).second->end(); - i++) { - transaction * t; - - if ((*i)->cost->commdty()) { - t = new transaction((*i)->acct, (*i)->cost); - } else { - amount * temp = (*x)->cost->value(); - t = new transaction((*i)->acct, temp->value((*i)->cost)); - delete temp; - } - - t->is_virtual = (*i)->is_virtual; - t->must_balance = (*i)->must_balance; - - new_xacts.push_back(t); - } - } - - // Add to the current entry any virtual transactions which were - // created. We have to do this afterward, otherwise the - // iteration above is screwed up if we try adding new - // transactions during the traversal. - - for (std::list<transaction *>::iterator x = new_xacts.begin(); - x != new_xacts.end(); - x++) { - xacts.push_back(*x); + for (transactions_list::const_iterator i = entry->transactions.begin(); + i != entry->transactions.end(); + i++) { + (*i)->account->add_transaction(*i); - if (do_compute) - (*x)->acct->balance.credit((*x)->cost); + if ((*i)->amount != (*i)->cost) { + assert((*i)->amount.commodity); + (*i)->amount.commodity->add_price(entry->date, (*i)->cost / (*i)->amount); } } - // Compute the balances again, just to make sure it all comes out - // right (i.e., zero for every commodity). - - if (! validate()) { - std::cerr << "Error, line " << (linenum - 1) - << ": Failed to balance the following transaction:" - << std::endl; - validate(true); - return false; - } - return true; } -bool entry::matches(const regexps_list& regexps) const -{ - if (regexps.empty() || (ledger::matches(regexps, code) || - ledger::matches(regexps, desc))) { - return true; - } - else { - bool match = false; - - for (std::list<transaction *>::const_iterator x = xacts.begin(); - x != xacts.end(); - x++) { - if (ledger::matches(regexps, (*x)->acct->as_str()) || - ledger::matches(regexps, (*x)->note)) { - match = true; - break; - } - } - return match; - } -} - -totals::~totals() -{ - for (iterator i = amounts.begin(); i != amounts.end(); i++) - delete (*i).second; -} - -void totals::credit(const amount * val) +bool ledger_t::remove_entry(entry_t * entry) { - iterator i = amounts.find(val->commdty()); - if (i != amounts.end()) - (*i).second->credit(val); -#ifndef DEBUG - else - amounts.insert(pair(val->commdty(), val->copy())); -#else - else { - std::pair<iterator, bool> result = - amounts.insert(pair(val->commdty(), val->copy())); - assert(result.second); - } -#endif -} + entries.remove(entry); -void totals::credit(const totals& other) -{ - for (const_iterator i = other.amounts.begin(); - i != other.amounts.end(); + for (transactions_list::const_iterator i + = entry->transactions.begin(); + i != entry->transactions.end(); i++) - credit((*i).second); -} + (*i)->account->remove_transaction(*i); -void totals::negate() -{ - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) - (*i).second->negate(); -} - -bool totals::is_zero() const -{ - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) - if (! (*i).second->is_zero()) - return false; return true; } -bool totals::is_negative() const -{ - bool all_negative = true; - bool some_negative = false; - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { - if ((*i).second->is_negative()) - some_negative = true; - else if (! (*i).second->is_zero()) - all_negative = false; - } - return some_negative && all_negative; -} - -void totals::print(std::ostream& out, int width) const -{ - bool first = true; - - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { - if ((*i).second->is_zero()) - continue; - - if (first) - first = false; - else - out << std::endl; - - out.width(width); - out << std::right << (*i).second->as_str(); - } -} - -totals * totals::value() const -{ - totals * cost_basis = new totals; - - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { - if ((*i).second->is_zero()) - continue; - - amount * value = (*i).second->value(); - cost_basis->credit(value); - delete value; - } - - return cost_basis; -} - -totals * totals::street(std::time_t * when, bool use_history, - bool download) const +int parse_ledger_file(char * p, ledger_t * book) { - totals * street_balance = new totals; - - for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { - if ((*i).second->is_zero()) - continue; - - amount * street = (*i).second->street(when, use_history, download); - street_balance->credit(street); - delete street; - } + char * sep = std::strrchr(p, '='); + if (sep) *sep++ = '\0'; - return street_balance; -} - -account::~account() -{ - for (accounts_map_iterator i = children.begin(); - i != children.end(); - i++) - delete (*i).second; -} + std::ifstream stream(p); -const std::string account::as_str(const account * stop) const -{ - if (! parent || this == stop) - return name; - else if (stop) - return parent->as_str(stop) + ":" + name; - else if (full_name.empty()) - full_name = parent->as_str() + ":" + name; - - return full_name; -} - -mask::mask(const std::string& pat) : exclude(false) -{ - const char * p = pat.c_str(); - if (*p == '-') { - exclude = true; - p++; - while (std::isspace(*p)) - p++; - } - else if (*p == '+') { - p++; - while (std::isspace(*p)) - p++; - } - pattern = p; - - const char *error; - int erroffset; - regexp = pcre_compile(pattern.c_str(), PCRE_CASELESS, - &error, &erroffset, NULL); - if (! regexp) - std::cerr << "Warning: Failed to compile regexp: " << pattern - << std::endl; -} - -mask::mask(const mask& m) : exclude(m.exclude), pattern(m.pattern) -{ - const char *error; - int erroffset; - regexp = pcre_compile(pattern.c_str(), PCRE_CASELESS, - &error, &erroffset, NULL); - assert(regexp); -} - -bool mask::match(const std::string& str) const -{ - static int ovec[30]; - int result = pcre_exec(regexp, NULL, str.c_str(), str.length(), - 0, 0, ovec, 30); - return result >= 0 && ! exclude; -} - -bool matches(const regexps_list& regexps, const std::string& str, - bool * by_exclusion) -{ - if (regexps.empty()) - return false; - - bool match = false; - bool definite = false; - - for (regexps_list_const_iterator r = regexps.begin(); - r != regexps.end(); - r++) { - static int ovec[30]; - int result = pcre_exec((*r).regexp, NULL, str.c_str(), str.length(), - 0, 0, ovec, 30); - if (result >= 0) { - match = ! (*r).exclude; - definite = true; - } - else if ((*r).exclude) { - if (! match) - match = ! definite; - } - else { - definite = true; - } - } - - if (by_exclusion) - *by_exclusion = match && ! definite && by_exclusion; - - return match; -} - -book::~book() -{ - for (commodities_map_iterator i = commodities.begin(); - i != commodities.end(); - i++) - delete (*i).second; - - for (accounts_map_iterator i = accounts.begin(); - i != accounts.end(); - i++) - delete (*i).second; - - for (virtual_map_iterator i = virtual_mapping.begin(); - i != virtual_mapping.end(); - i++) { - delete (*i).first; - - for (std::list<transaction *>::iterator j = (*i).second->begin(); - j != (*i).second->end(); - j++) { - delete *j; - } - delete (*i).second; - } - - for (entries_list_iterator i = entries.begin(); - i != entries.end(); - i++) - delete *i; -} - -account * book::re_find_account(const std::string& regex) -{ - mask acct_regex(regex); - - for (entries_list_reverse_iterator i = entries.rbegin(); - i != entries.rend(); - i++) - for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); - x != (*i)->xacts.end(); - x++) - if (acct_regex.match((*x)->acct->as_str())) - return (*x)->acct; - - return NULL; -} - -account * book::find_account(const std::string& name, bool create) -{ - accounts_map_iterator i = accounts_cache.find(name); - if (i != accounts_cache.end()) - return (*i).second; - - char * buf = new char[name.length() + 1]; - std::strcpy(buf, name.c_str()); - - account * current = NULL; - for (char * tok = std::strtok(buf, ":"); - tok; - tok = std::strtok(NULL, ":")) { - if (! current) { - accounts_map_iterator i = accounts.find(tok); - if (i == accounts.end()) { - if (! create) { - delete[] buf; - return NULL; - } - current = new account(tok); - accounts.insert(accounts_map_pair(tok, current)); - } else { - current = (*i).second; - } - } else { - accounts_map_iterator i = current->children.find(tok); - if (i == current->children.end()) { - if (! create) { - delete[] buf; - return NULL; - } - current = new account(tok, current); - current->parent->children.insert(accounts_map_pair(tok, current)); - } else { - current = (*i).second; - } - } - } + account_t * master; + if (sep) + master = book->find_account(sep); + else + master = book->master; - delete[] buf; + book->sources.push_back(p); - if (current) - accounts_cache.insert(accounts_map_pair(name, current)); + unsigned long magic; + std::istream::pos_type start = stream.tellg(); + stream.read((char *)&magic, sizeof(magic)); + stream.seekg(start); - return current; + if (magic == magic_number) + return read_binary_ledger(stream, "", book, master); + else + return parse_textual_ledger(stream, book, master); } } // namespace ledger diff --git a/ledger.el b/ledger.el deleted file mode 100644 index ac52c37c..00000000 --- a/ledger.el +++ /dev/null @@ -1,295 +0,0 @@ -;;; ledger.el --- Helper code for using my "ledger" command-line tool - -;; Copyright (C) 2004 John Wiegley (johnw AT gnu DOT org) - -;; Emacs Lisp Archive Entry -;; Filename: ledger.el -;; Version: 1.1 -;; Date: Thu 02-Apr-2004 -;; Keywords: data -;; Author: John Wiegley (johnw AT gnu DOT org) -;; Maintainer: John Wiegley (johnw AT gnu DOT org) -;; Description: Helper code for using my "ledger" command-line tool -;; URL: http://www.newartisans.com/johnw/emacs.html -;; Compatibility: Emacs21 - -;; This file is not part of GNU Emacs. - -;; This is free software; you can redistribute it and/or modify it under -;; the terms of the GNU General Public License as published by the Free -;; Software Foundation; either version 2, or (at your option) any later -;; version. -;; -;; This is distributed in the hope that it will be useful, but WITHOUT -;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -;; for more details. -;; -;; You should have received a copy of the GNU General Public License -;; along with GNU Emacs; see the file COPYING. If not, write to the -;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, -;; MA 02111-1307, USA. - -;;; Commentary: - -;; This code is only meaningful if you are using "ledger". - -(defvar ledger-version "1.1" - "The version of ledger.el currently loaded") - -(defgroup ledger nil - "Interface to the Ledger command-line accounting program." - :group 'data) - -(defcustom ledger-binary-path (executable-find "ledger") - "Path to the ledger executable." - :type 'file - :group 'ledger) - -(defvar bold 'bold) - -(defvar ledger-font-lock-keywords - '(("^[0-9./]+\\s-+\\(?:([^)]+)\\s-+\\)?\\([^*].+\\)" 1 bold) - ("^\\s-+.+?\\( \\|\t\\|\\s-+$\\)" . font-lock-keyword-face)) - "Default expressions to highlight in Ledger mode.") - -(defun ledger-iterate-entries (callback) - (goto-char (point-min)) - (let* ((now (current-time)) - (current-year (nth 5 (decode-time now)))) - (while (not (eobp)) - (when (looking-at - (concat "\\(Y\\s-+\\([0-9]+\\)\\|" - "\\([0-9]\\{4\\}+\\)?[./]?" - "\\([0-9]+\\)[./]\\([0-9]+\\)\\s-+" - "\\(\\*\\s-+\\)?\\(.+\\)\\)")) - (let ((found (match-string 2))) - (if found - (setq current-year (string-to-number found)) - (let ((start (match-beginning 0)) - (year (match-string 3)) - (month (string-to-number (match-string 4))) - (day (string-to-number (match-string 5))) - (mark (match-string 6)) - (desc (match-string 7))) - (if (and year (> (length year) 0)) - (setq year (string-to-number year))) - (funcall callback start - (encode-time 0 0 0 day month - (or year current-year)) - mark desc))))) - (forward-line)))) - -(defun ledger-find-slot (moment) - (catch 'found - (ledger-iterate-entries - (function - (lambda (start date mark desc) - (if (time-less-p moment date) - (throw 'found t))))))) - -(defun ledger-add-entry (entry) - (interactive - (list (read-string "Entry: " (format-time-string "%Y/%m/%d ")))) - (let* ((args (mapcar 'shell-quote-argument (split-string entry))) - (date (car args)) - (insert-year t) exit-code) - (if (string-match "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]+\\)" date) - (setq date (encode-time 0 0 0 (string-to-int (match-string 3 date)) - (string-to-int (match-string 2 date)) - (string-to-int (match-string 1 date))))) - (ledger-find-slot date) - (save-excursion - (if (re-search-backward "^Y " nil t) - (setq insert-year nil))) - (save-excursion - (insert - (with-temp-buffer - (setq exit-code - (apply 'call-process ledger-binary-path nil t nil - (cons "entry" args))) - (if (= 0 exit-code) - (if insert-year - (buffer-string) - (buffer-substring 5 (point-max))) - (concat (if insert-year entry - (substring entry 5)) "\n\n"))))))) - -(defun ledger-expand-entry () - (interactive) - (ledger-add-entry (prog1 - (buffer-substring (line-beginning-position) - (line-end-position)) - (delete-region (line-beginning-position) - (1+ (line-end-position)))))) - -(defun ledger-toggle-current () - (interactive) - (let (clear) - (save-excursion - (when (or (looking-at "^[0-9]") - (re-search-backward "^[0-9]" nil t)) - (skip-chars-forward "0-9./") - (delete-horizontal-space) - (if (equal ?\* (char-after)) - (delete-char 1) - (insert " * ") - (setq clear t)))) - clear)) - -(defun ledger-print-result (command) - (interactive "sLedger command: ") - (shell-command (format "%s -f %s %s" ledger-binary-path - buffer-file-name command))) - -(define-derived-mode ledger-mode text-mode "Ledger" - "A mode for editing ledger data files." - (set (make-local-variable 'comment-start) ";") - (set (make-local-variable 'comment-end) "") - (set (make-local-variable 'indent-tabs-mode) nil) - (if (boundp 'font-lock-defaults) - (set (make-local-variable 'font-lock-defaults) - '(ledger-font-lock-keywords nil t))) - (let ((map (current-local-map))) - (define-key map [(control ?c) (control ?a)] 'ledger-add-entry) - (define-key map [(control ?c) (control ?c)] 'ledger-toggle-current) - (define-key map [(control ?c) (control ?p)] 'ledger-print-result) - (define-key map [(control ?c) (control ?r)] 'ledger-reconcile))) - -(defun ledger-parse-entries (account &optional all-p after-date) - (let (total entries) - (ledger-iterate-entries - (function - (lambda (start date mark desc) - (when (and (or all-p (not mark)) - (time-less-p after-date date)) - (forward-line) - (setq total 0.0) - (while (looking-at - (concat "\\s-+\\([A-Za-z_].+?\\)\\(\\s-*$\\| \\s-*" - "\\([^0-9]+\\)\\s-*\\([0-9,.]+\\)\\)?" - "\\(\\s-+;.+\\)?$")) - (let ((acct (match-string 1)) - (amt (match-string 4))) - (when amt - (while (string-match "," amt) - (setq amt (replace-match "" nil nil amt))) - (setq amt (string-to-number amt) - total (+ total amt))) - (if (string= account acct) - (setq entries - (cons (list (copy-marker start) - mark date desc (or amt total)) - entries)))) - (forward-line)))))) - entries)) - -(defvar ledger-reconcile-text "Reconcile") - -(define-derived-mode ledger-reconcile-mode text-mode 'ledger-reconcile-text - "A mode for reconciling ledger entries." - (let ((map (make-sparse-keymap))) - (define-key map [? ] 'ledger-reconcile-toggle) - (define-key map [?q] - (function - (lambda () - (interactive) - (kill-buffer (current-buffer))))) - (use-local-map map))) - -(add-to-list 'minor-mode-alist - '(ledger-reconcile-mode ledger-reconcile-text)) - -(defvar ledger-buf nil) -(defvar ledger-acct nil) - -(defun ledger-update-balance-display () - (let ((account ledger-acct)) - (with-temp-buffer - (let ((exit-code - (apply 'call-process ledger-binary-path nil t nil - (list "-C" "balance" account)))) - (if (/= 0 exit-code) - (setq ledger-reconcile-text "Reconcile [ERR]") - (goto-char (point-min)) - (delete-horizontal-space) - (skip-syntax-forward "^ ") - (setq ledger-reconcile-text - (concat "Reconcile [" - (buffer-substring-no-properties (point-min) (point)) - "]")))))) - (redraw-modeline)) - -(defun ledger-reconcile-toggle () - (interactive) - (let ((where (get-text-property (point) 'where)) - (account ledger-acct) - cleared) - (with-current-buffer ledger-buf - (goto-char where) - (setq cleared (ledger-toggle-current)) - (save-buffer)) - (if cleared - (add-text-properties (line-beginning-position) - (line-end-position) - (list 'face 'bold)) - (remove-text-properties (line-beginning-position) - (line-end-position) - (list 'face))) - (forward-line) - (ledger-update-balance-display))) - -(defun ledger-reconcile (account &optional days) - (interactive "sAccount to reconcile: \nnBack how far (default 30 days): ") - (let* ((then (time-subtract (current-time) - (seconds-to-time (* (or days 30) 24 60 60)))) - (items (save-excursion - (goto-char (point-min)) - (ledger-parse-entries account t then))) - (buf (current-buffer))) - (with-current-buffer - (pop-to-buffer (generate-new-buffer "*Reconcile*")) - (ledger-reconcile-mode) - (set (make-local-variable 'ledger-buf) buf) - (set (make-local-variable 'ledger-acct) account) - (ledger-update-balance-display) - (dolist (item items) - (let ((beg (point))) - (insert (format "%s %-30s %8.2f\n" - (format-time-string "%Y/%m/%d" (nth 2 item)) - (nth 3 item) (nth 4 item))) - (if (nth 1 item) - (set-text-properties beg (1- (point)) - (list 'face 'bold - 'where (nth 0 item))) - (set-text-properties beg (1- (point)) - (list 'where (nth 0 item))))) - (goto-char (point-min)))))) - -(defun ledger-align-dollars (&optional column) - (interactive "p") - (if (= column 1) - (setq column 48)) - (while (search-forward "$" nil t) - (backward-char) - (let ((col (current-column)) - (beg (point)) - target-col len) - (skip-chars-forward "-$0-9,.") - (setq len (- (point) beg)) - (setq target-col (- column len)) - (if (< col target-col) - (progn - (goto-char beg) - (insert (make-string (- target-col col) ? ))) - (move-to-column target-col) - (if (looking-back " ") - (delete-char (- col target-col)) - (skip-chars-forward "^ \t") - (delete-horizontal-space) - (insert " "))) - (forward-line)))) - -(provide 'ledger) - -;;; ledger.el ends here @@ -1,5 +1,5 @@ #ifndef _LEDGER_H -#define _LEDGER_H "$Revision: 1.34 $" +#define _LEDGER_H ////////////////////////////////////////////////////////////////////// // @@ -7,15 +7,16 @@ // // A command-line tool for general double-entry accounting. // -// Copyright (c) 2003 John Wiegley <johnw@newartisans.com> +// Copyright (c) 2003,2004 John Wiegley <johnw@newartisans.com> // -#include <iostream> -#include <string> -#include <vector> -#include <list> #include <map> +#include <list> +#include <string> #include <ctime> +#include <cctype> +#include <iostream> +#include <sstream> #ifdef DEBUG #include <cassert> @@ -26,333 +27,405 @@ #define assert(x) #endif -#include <pcre.h> // Perl regular expression library - namespace ledger { -class amount; -class commodity -{ - commodity(const commodity&); - - typedef std::map<const std::time_t, amount *> price_map; - typedef std::pair<const std::time_t, amount *> price_map_pair; - - public: - std::string name; - std::string symbol; +extern const std::string version; - mutable bool sought; +class commodity_t; +class amount_t; +class transaction_t; +class entry_t; +class account_t; +class ledger_t; - bool prefix; - bool separate; - bool thousands; - bool european; - int precision; +class amount_t +{ + typedef void * base_type; - protected: - mutable price_map history; // the price history - mutable amount * conversion; // fixed conversion (ignore history) + void _init(); + void _copy(const amount_t& amt); + void _clear(); public: - explicit commodity() : sought(false), prefix(false), separate(true), - thousands(false), european(false), conversion(NULL) {} - - explicit commodity(const std::string& sym, bool pre = false, - bool sep = true, bool thou = true, - bool euro = false, int prec = 2); - ~commodity(); + base_type quantity; // amount, to MAX_PRECISION + commodity_t * commodity; - void set_price(amount * price, std::time_t * when = NULL); - amount * price(std::time_t * when = NULL, - bool use_history = false, bool download = false) const; -}; + static commodity_t * null_commodity; -typedef std::map<const std::string, commodity *> commodities_map; -typedef commodities_map::iterator commodities_map_iterator; -typedef std::pair<const std::string, commodity *> commodities_map_pair; + bool valid() const { + if (quantity) + return commodity != NULL; + else + return commodity == NULL; + } + // constructors + amount_t(commodity_t * _commodity = NULL) + : quantity(NULL), commodity(_commodity) {} -class amount -{ - public: - virtual ~amount() {} + amount_t(const amount_t& amt) : quantity(NULL) { + if (amt.quantity) + _copy(amt); + else + commodity = amt.commodity; + } + amount_t(const std::string& value) { + _init(); + std::istringstream str(value); + str >> *this; + } + amount_t(const int value) : quantity(NULL), commodity(NULL) { + if (value != 0) { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + } + amount_t(const unsigned int value) : quantity(NULL), commodity(NULL) { + if (value != 0) { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + } + amount_t(const double value) : quantity(NULL), commodity(NULL) { + if (value != 0.0) { + std::string str; + std::ostringstream strstr(str); + strstr << value; + parse(strstr.str()); + } + } - virtual commodity * commdty() const = 0; - virtual void set_commdty(commodity *) = 0; + // destructor + ~amount_t() { + if (quantity) + _clear(); + } - virtual amount * copy() const = 0; - virtual amount * value(const amount * pr = NULL) const = 0; - virtual void set_value(const amount * pr) = 0; - virtual amount * street(std::time_t * when = NULL, - bool use_history = false, - bool download = false) const = 0; + // assignment operator + amount_t& operator=(const amount_t& amt); + amount_t& operator=(const std::string& value); + amount_t& operator=(const int value); + amount_t& operator=(const unsigned int value); + amount_t& operator=(const double value); + + // general methods + amount_t round(int precision = -1) const; + + // in-place arithmetic + amount_t& operator*=(const amount_t& amt); + amount_t& operator/=(const amount_t& amt); + amount_t& operator%=(const amount_t& amt); + amount_t& operator+=(const amount_t& amt); + amount_t& operator-=(const amount_t& amt); + + // simple arithmetic + amount_t operator*(const amount_t& amt) const { + amount_t temp = *this; + temp *= amt; + return temp; + } + amount_t operator/(const amount_t& amt) const { + amount_t temp = *this; + temp /= amt; + return temp; + } + amount_t operator%(const amount_t& amt) const { + amount_t temp = *this; + temp %= amt; + return temp; + } + amount_t operator+(const amount_t& amt) const { + amount_t temp = *this; + temp += amt; + return temp; + } + amount_t operator-(const amount_t& amt) const { + amount_t temp = *this; + temp -= amt; + return temp; + } - virtual bool has_price() const = 0; - virtual amount * per_item_price() const = 0; + // unary negation + amount_t& negate(); + amount_t negated() const { + amount_t temp = *this; + temp.negate(); + return temp; + } + amount_t operator-() const { + return negated(); + } - // Comparison + // test for non-zero (use ! for zero) + operator bool() const; + + // comparisons to zero + bool operator<(const int num) const; + bool operator<=(const int num) const; + bool operator>(const int num) const; + bool operator>=(const int num) const; + + // comparisons between amounts + bool operator<(const amount_t& amt) const; + bool operator<=(const amount_t& amt) const; + bool operator>(const amount_t& amt) const; + bool operator>=(const amount_t& amt) const; + bool operator==(const amount_t& amt) const; + bool operator!=(const amount_t& amt) const { + if (commodity != amt.commodity) + return true; + return ! (*this == amt); + } - virtual bool is_zero() const = 0; - virtual bool is_negative() const = 0; - virtual int compare(const amount * other) const = 0; + amount_t value(const std::time_t moment) const; - // Assignment + operator std::string() const; - virtual void credit(const amount * other) = 0; - virtual void negate() = 0; + void parse(std::istream& in, ledger_t * ledger = NULL); + void parse(const std::string& str, ledger_t * ledger = NULL) { + std::istringstream stream(str); + parse(stream, ledger); + } - // String conversion routines + void write_quantity(std::ostream& out) const; + void read_quantity(std::istream& in); - virtual void parse(const std::string& num) = 0; - virtual const std::string as_str(bool full_prec = false) const = 0; + friend std::istream& operator>>(std::istream& in, amount_t& amt); }; -extern amount * create_amount(const std::string& value, - const amount * cost = NULL); +void parse_quantity(std::istream& in, std::string& value); +void parse_commodity(std::istream& in, std::string& symbol); -class mask -{ - public: - bool exclude; - std::string pattern; - pcre * regexp; +inline amount_t abs(const amount_t& amt) { + return amt < 0 ? amt.negated() : amt; +} - explicit mask(const std::string& pattern); +inline std::istream& operator>>(std::istream& in, amount_t& amt) { + amt.parse(in); + return in; +} - mask(const mask&); +std::ostream& operator<<(std::ostream& out, const amount_t& amt); - ~mask() { - pcre_free(regexp); - } - bool match(const std::string& str) const; -}; +#define COMMODITY_STYLE_DEFAULTS 0x00 +#define COMMODITY_STYLE_SUFFIXED 0x01 +#define COMMODITY_STYLE_SEPARATED 0x02 +#define COMMODITY_STYLE_EUROPEAN 0x04 +#define COMMODITY_STYLE_THOUSANDS 0x08 +#define COMMODITY_STYLE_CONSULTED 0x10 +#define COMMODITY_STYLE_NOMARKET 0x20 -typedef std::list<mask> regexps_list; -typedef std::list<mask>::iterator regexps_list_iterator; -typedef std::list<mask>::const_iterator regexps_list_const_iterator; +typedef std::map<const std::time_t, amount_t> history_map; +typedef std::pair<const std::time_t, amount_t> history_pair; -class account; -class transaction +class commodity_t { - transaction(const transaction&); - public: - account * acct; - amount * cost; - - std::string note; - - bool is_virtual; - bool must_balance; - bool specified; - - explicit transaction(account * _acct = NULL, amount * _cost = NULL) - : acct(_acct), cost(_cost), - is_virtual(false), must_balance(true), specified(false) {} - - ~transaction() { - if (cost) - delete cost; + std::string symbol; + std::string name; + std::string note; + unsigned int precision; + unsigned int flags; + history_map history; + amount_t conversion; + unsigned long ident; + + static void (*updater)(commodity_t * commodity, + const std::time_t date, + const amount_t& price, + const std::time_t moment); + + commodity_t(const std::string& _symbol = "", + unsigned int _precision = 2, + unsigned int _flags = COMMODITY_STYLE_DEFAULTS) + : symbol(_symbol), precision(_precision), flags(_flags) {} + + void add_price(const std::time_t date, const amount_t& price) { + history.insert(history_pair(date, price)); + } + bool remove_price(const std::time_t date) { + history_map::size_type n = history.erase(date); + return n > 0; } - const std::string acct_as_str() const; + void set_conversion(const amount_t& price) { + conversion = price; + } - void print(std::ostream& out, bool display_quantity = true, - bool display_price = true) const; + amount_t value(const std::time_t moment = std::time(NULL)); }; -class book; -class entry -{ - entry(const entry&); +#define TRANSACTION_NORMAL 0x0 +#define TRANSACTION_VIRTUAL 0x1 +#define TRANSACTION_BALANCE 0x2 +class transaction_t +{ public: - book * ledger; - - std::time_t date; - std::string code; - std::string desc; - - bool cleared; - - std::list<transaction *> xacts; + entry_t * entry; + account_t * account; + amount_t amount; + amount_t cost; + unsigned int flags; + std::string note; + + transaction_t(entry_t * _entry, account_t * _account) + : entry(_entry), account(_account), flags(TRANSACTION_NORMAL) {} + + transaction_t(entry_t * _entry, + account_t * _account, + const amount_t& _amount, + const amount_t& _cost, + unsigned int _flags = TRANSACTION_NORMAL, + const std::string& _note = "") + : entry(_entry), account(_account), amount(_amount), + cost(_cost), flags(_flags), note(_note) {} +}; - explicit entry(book * l) : ledger(l), cleared(false) {} - // If we're running as a command-line tool, it's cheaper to just - // throw away the heap on exit, than spend time freeing things up - // like a good citizen. +typedef std::list<transaction_t *> transactions_list; - ~entry() { - for (std::list<transaction *>::iterator i = xacts.begin(); - i != xacts.end(); - i++) { +class entry_t +{ + public: + enum entry_state_t { + UNCLEARED, CLEARED, PENDING + }; + + std::time_t date; + enum entry_state_t state; + std::string code; + std::string payee; + transactions_list transactions; + + ~entry_t() { + for (transactions_list::iterator i = transactions.begin(); + i != transactions.end(); + i++) delete *i; - } } - bool matches(const regexps_list& regexps) const; - bool validate(bool show_unaccounted = false) const; - bool finalize(bool do_compute = false); - - void print(std::ostream& out, bool shortcut = true) const; -}; - -struct cmp_entry_date { - bool operator()(const entry * left, const entry * right) { - return std::difftime(left->date, right->date) < 0; + void add_transaction(transaction_t * xact) { + transactions.push_back(xact); + } + bool remove_transaction(transaction_t * xact) { + transactions.remove(xact); + return true; } }; -typedef std::vector<entry *> entries_list; -typedef entries_list::iterator entries_list_iterator; -typedef entries_list::reverse_iterator entries_list_reverse_iterator; -typedef entries_list::const_iterator entries_list_const_iterator; +typedef std::map<const std::string, account_t *> accounts_map; +typedef std::pair<const std::string, account_t *> accounts_pair; -class totals -{ - totals(const totals&); +inline std::ostream& operator<<(std::ostream& out, const account_t& acct); +class account_t +{ public: - typedef std::map<commodity *, amount *> map; - typedef map::iterator iterator; - typedef map::const_iterator const_iterator; - typedef std::pair<commodity *, amount *> pair; - - map amounts; - - totals() {} - ~totals(); - - void credit(const amount * val); - void credit(const totals& other); + const account_t * parent; + std::string name; + std::string note; + accounts_map accounts; + mutable accounts_map accounts_cache; + transactions_list transactions; + unsigned long ident; + static unsigned long next_ident; + + account_t(const account_t * _parent, const std::string& _name = "", + const std::string& _note = "") + : parent(_parent), name(_name), note(_note) {} + + ~account_t(); + + std::string fullname() const; + + void add_account(account_t * acct) { + acct->ident = next_ident++; + accounts.insert(accounts_pair(acct->name, acct)); + } + bool remove_account(account_t * acct) { + accounts_map::size_type n = accounts.erase(acct->name); + return n > 0; + } - void negate(); + account_t * find_account(const std::string& name, bool auto_create = true); - bool is_zero() const; - bool is_negative() const; + operator std::string() const { + return fullname(); + } - void print(std::ostream& out, int width) const; + // These functions should only be called from ledger_t::add_entry + // and ledger_t::remove_entry; or from the various parsers. + void add_transaction(transaction_t * xact) { + transactions.push_back(xact); + } + bool remove_transaction(transaction_t * xact); - totals * value() const; - totals * street(std::time_t * when = NULL, - bool use_history = false, - bool download = false) const; + friend class ledger_t; }; +inline std::ostream& operator<<(std::ostream& out, const account_t& acct) { + out << acct.fullname(); + return out; +} -typedef std::map<const std::string, account *> accounts_map; -typedef accounts_map::iterator accounts_map_iterator; -typedef std::pair<const std::string, account *> accounts_map_pair; - -class account -{ - account(const account&); - - public: - account * parent; - - std::string name; -#ifdef READ_GNUCASH - commodity * comm; // default commodity for this account -#endif - totals balance; // optional, parse-time computed balance - int checked; // 'balance' uses this for speed's sake - accounts_map children; - - mutable std::string full_name; - - explicit account() : parent(NULL), checked(0) {} - - explicit account(const std::string& _name, - struct account * _parent = NULL) - : parent(_parent), name(_name), checked(0) {} - - ~account(); - const std::string as_str(const account * stop = NULL) const; -}; +typedef std::map<const std::string, commodity_t *> commodities_map; +typedef std::pair<const std::string, commodity_t *> commodities_pair; +typedef std::list<entry_t *> entries_list; -class book +class ledger_t { - book(const book&); - public: - typedef std::map<regexps_list *, - std::list<transaction *> *> virtual_map; - - typedef std::pair<regexps_list *, - std::list<transaction *> *> virtual_map_pair; - - typedef virtual_map::const_iterator virtual_map_iterator; - - commodities_map commodities; - accounts_map accounts; - accounts_map accounts_cache; // maps full names to accounts - virtual_map virtual_mapping; - entries_list entries; - int current_year; - - book() {} - ~book(); - - template<typename Compare> - void sort(Compare comp) { - std::sort(entries.begin(), entries.end(), comp); + account_t * master; + commodities_map commodities; + entries_list entries; + std::list<std::string> sources; + + ledger_t() { + master = new account_t(NULL, ""); + master->ident = 0; + account_t::next_ident = 1; } - void print(std::ostream& out, regexps_list& regexps, - bool shortcut) const; - account * re_find_account(const std::string& regex); - account * find_account(const std::string& name, bool create = true); -}; - -extern book * main_ledger; + ~ledger_t(); -inline commodity::commodity(const std::string& sym, bool pre, bool sep, - bool thou, bool euro, int prec) - : symbol(sym), sought(false), prefix(pre), separate(sep), - thousands(thou), european(euro), precision(prec), conversion(NULL) { -#ifdef DEBUG - std::pair<commodities_map_iterator, bool> result = -#endif - main_ledger->commodities.insert(commodities_map_pair(sym, this)); -#ifdef DEBUG - assert(result.second); -#endif -} - -// Parsing routines + void add_account(account_t * acct) { + master->add_account(acct); + } + bool remove_account(account_t * acct) { + return master->remove_account(acct); + } -extern int parse_ledger(book * ledger, std::istream& in, - regexps_list& regexps, - bool compute_balances = false, - const char * acct_prefix = NULL); -#ifdef READ_GNUCASH -extern book * parse_gnucash(std::istream& in, bool compute_balances = false); -#endif + account_t * find_account(const std::string& name, bool auto_create = true) { + return master->find_account(name, auto_create); + } -extern int parse_ledger_file(book * ledger, const std::string& file, - regexps_list& regexps, - bool compute_balances = false, - const char * acct_prefix = NULL); + void add_commodity(commodity_t * commodity, const std::string symbol = "") { + commodities.insert(commodities_pair(symbol.empty() ? + commodity->symbol : symbol, commodity)); + } + bool remove_commodity(commodity_t * commodity) { + commodities_map::size_type n = commodities.erase(commodity->symbol); + return n > 0; + } -extern bool parse_date_mask(const char * date_str, - struct std::tm * result); -extern bool parse_date(const char * date_str, std::time_t * result, - const int year = -1); + commodity_t * find_commodity(const std::string& symbol, + bool auto_create = false); -extern void record_regexp(const std::string& pattern, regexps_list& regexps); -extern void read_regexps(const std::string& path, regexps_list& regexps); -extern bool matches(const regexps_list& regexps, const std::string& str, - bool * by_exclusion = NULL); + bool add_entry(entry_t * entry); + bool remove_entry(entry_t * entry); +}; -extern void parse_price_setting(const std::string& setting); +int parse_ledger_file(char * p, ledger_t * book); } // namespace ledger diff --git a/main.cc b/main.cc new file mode 100644 index 00000000..70a3f015 --- /dev/null +++ b/main.cc @@ -0,0 +1,899 @@ +#include "ledger.h" +#include "constraint.h" +#include "textual.h" +#include "binary.h" +#include "balance.h" +#include "report.h" + +#include <fstream> +#include <cstring> +#include <unistd.h> +#include <ctime> + +namespace ledger { + + +////////////////////////////////////////////////////////////////////// +// +// The command-line balance report +// + +static const std::string bal_fmt = "%20T%2_%-n\n"; + +void show_balances(std::ostream& out, + report::items_deque& items, + const constraints_t& constraints, + const report::format_t& format, + const report::item_t * displayed_parent) +{ + if (format.sort_order) + std::sort(items.begin(), items.end(), report::cmp_items(format)); + + for (report::items_deque::const_iterator i = items.begin(); + i != items.end(); + i++) { + const report::item_t * parent = displayed_parent; + + bool by_exclusion = false; + std::string name = maximal_account_name(*i, parent); + const bool match = (format.show_expanded || + (! constraints.account_masks.empty() && + matches(constraints.account_masks, name, + &by_exclusion) && + (! by_exclusion || + displayed_parent->parent == NULL)) || + (constraints.account_masks.empty() && + displayed_parent->parent == NULL)); + + if (match && reportable(format, *i, true) && + ((*i)->subitems.size() != 1 || + (*i)->total != (*i)->subitems[0]->total)) { + out << format_string(*i, format, parent); + parent = *i; + } + + show_balances(out, (*i)->subitems, constraints, format, parent); + } +} + +void balance_report(std::ostream& out, + report::item_t * top, + const constraints_t& constraints, + const report::format_t& format) +{ + show_balances(out, top->subitems, constraints, format, top); + + if (format.show_subtotals && top->subitems.size() > 1 && top->total) + std::cout << "--------------------\n" + << report::format_string(top, format); +} + + +////////////////////////////////////////////////////////////////////// +// +// The command-line register report +// + +static const std::string reg_fmt + = "%?10d %?-.20p %/%-.22N %12.66t %12.80T\n"; + +static bool show_commodities_revalued = false; +static bool show_commodities_revalued_only = false; + +static void report_value_change(std::ostream& out, + const std::time_t date, + const balance_pair_t& balance, + const balance_pair_t& prev_balance, + report::format_t& format, + const std::string& first_line_format, + const std::string& next_lines_format) +{ + static std::time_t prev_date = -1; + + if (prev_date == -1) { + prev_date = date; + return; + } + + report::item_t temp; + temp.date = prev_date; + temp.total = prev_balance; + balance_t prev_bal = format.compute_total(&temp); + + temp.date = date; + temp.total = balance; + balance_t cur_bal = format.compute_total(&temp); + + if (balance_t diff = cur_bal - prev_bal) { + temp.value = diff; + temp.total = balance; + temp.payee = "Commodities revalued"; + + if (reportable(format, &temp)) { + format.format_string = first_line_format; + out << format_string(&temp, format, NULL); + + format.format_string = next_lines_format; + out << format_string(&temp, format, NULL); + } + } + + prev_date = date; +} + +void register_report(std::ostream& out, report::item_t * top, + const report::format_t& format) +{ + if (format.sort_order) + std::sort(top->subitems.begin(), top->subitems.end(), + report::cmp_items(format)); + + report::format_t copy = format; + + std::string first_line_format; + std::string next_lines_format; + + const char * f = format.format_string.c_str(); + if (const char * p = std::strstr(f, "%/")) { + first_line_format = std::string(f, 0, p - f); + next_lines_format = std::string(p + 2); + } else { + first_line_format = format.format_string; + next_lines_format = format.format_string; + } + + balance_pair_t balance; + balance_pair_t last_reported; + account_t splits(NULL, "<Total>"); + + for (report::items_deque::const_iterator i = top->subitems.begin(); + i != top->subitems.end(); + i++) { + copy.format_string = first_line_format; + + std::string header = format_string(*i, copy, top); + unsigned int header_len = header.length(); + + copy.format_string = next_lines_format; + + bool first = true; + + if ((*i)->subitems.size() > 1 && ! format.show_expanded) { + report::item_t summary; + summary.date = (*i)->date; + summary.parent = *i; + summary.account = &splits; + + for (report::items_deque::const_iterator j = (*i)->subitems.begin(); + j != (*i)->subitems.end(); + j++) + summary.value += (*j)->value; + summary.total = balance + summary.value; + + bool show = reportable(format, &summary); + if (show && show_commodities_revalued) + report_value_change(out, summary.date, balance, last_reported, copy, + first_line_format, next_lines_format); + + balance += summary.value; + + if (show) { + if (! show_commodities_revalued_only) + out << header << format_string(&summary, copy, *i); + if (show_commodities_revalued) + last_reported = balance; + } + } else { + for (report::items_deque::const_iterator j = (*i)->subitems.begin(); + j != (*i)->subitems.end(); + j++) { + (*j)->total = balance + (*j)->value; + + bool show = reportable(format, *j); + if (show && first && show_commodities_revalued) { + report_value_change(out, (*i)->date, balance, last_reported, copy, + first_line_format, next_lines_format); + if (show_commodities_revalued_only) + first = false; + } + + balance += (*j)->value; + + if (show) { + if (! show_commodities_revalued_only) { + if (first) { + first = false; + out << header; + } else { + out.width(header_len); + out << " "; + } + out << format_string(*j, copy, *i); + } + if (show_commodities_revalued) + last_reported = balance; + } + } + } + } + + if (show_commodities_revalued) + report_value_change(out, copy.end(), balance, last_reported, copy, + first_line_format, next_lines_format); + + // To stop these from getting deleted when copy goes out of scope + copy.predicate = NULL; + copy.sort_order = NULL; + copy.value_style = NULL; + copy.total_style = NULL; +} + + +bool add_new_entry(int index, int argc, char **argv, ledger_t * ledger) +{ + masks_list regexps; + entry_t added; + entry_t * matching = NULL; + + added.state = entry_t::UNCLEARED; + + assert(index < argc); + + if (! parse_date(argv[index++], &added.date)) { + std::cerr << "Error: Bad entry date: " << argv[index - 1] + << std::endl; + return false; + } + + if (index == argc) { + std::cerr << "Error: Too few arguments to 'entry'." << std::endl; + return false; + } + + regexps.push_back(mask_t(argv[index++])); + + for (entries_list::reverse_iterator i = ledger->entries.rbegin(); + i != ledger->entries.rend(); + i++) + if (matches(regexps, (*i)->payee)) { + matching = *i; + break; + } + + added.payee = matching ? matching->payee : regexps.front().pattern; + + if (index == argc) { + std::cerr << "Error: Too few arguments to 'entry'." << std::endl; + return false; + } + + if (argv[index][0] == '-' || std::isdigit(argv[index][0])) { + if (! matching) { + std::cerr << "Error: Missing account name for non-matching entry." + << std::endl; + return false; + } + + transaction_t * m_xact, * xact, * first; + m_xact = matching->transactions.front(); + + amount_t amt(argv[index++]); + first = xact = new transaction_t(&added, m_xact->account, amt, amt); + + if (xact->amount.commodity->symbol.empty()) { + xact->amount.commodity = m_xact->amount.commodity; + xact->cost.commodity = m_xact->amount.commodity; + } + added.add_transaction(xact); + + m_xact = matching->transactions.back(); + + xact = new transaction_t(&added, m_xact->account, + - first->amount, - first->amount); + added.add_transaction(xact); + + if ((index + 1) < argc && std::string(argv[index]) == "-from") + if (account_t * acct = ledger->find_account(argv[++index])) + added.transactions.back()->account = acct; + } else { + while (index < argc && std::string(argv[index]) != "-from") { + mask_t acct_regex(argv[index++]); + + account_t * acct = NULL; + commodity_t * cmdty = NULL; + + if (matching) { + for (transactions_list::iterator x + = matching->transactions.begin(); + x != matching->transactions.end(); + x++) { + if (acct_regex.match((*x)->account->fullname())) { + acct = (*x)->account; + cmdty = (*x)->amount.commodity; + break; + } + } + } + + if (! acct) + acct = ledger->find_account(acct_regex.pattern); + + if (! acct) { + std::cerr << "Error: Could not find account name '" + << acct_regex.pattern << "'." << std::endl; + return false; + } + + if (index == argc) { + std::cerr << "Error: Too few arguments to 'entry'." << std::endl; + return false; + } + + amount_t amt(argv[index]++); + transaction_t * xact = new transaction_t(&added, acct, amt, amt); + + if (! xact->amount.commodity) + xact->amount.commodity = cmdty; + + added.add_transaction(xact); + } + + if ((index + 1) < argc && std::string(argv[index]) == "-from") { + if (account_t * acct = ledger->find_account(argv[++index])) { + transaction_t * xact = new transaction_t(NULL, acct); + added.add_transaction(xact); + } + } else { + if (! matching) { + std::cerr << "Error: Could not figure out the account to draw from." + << std::endl; + std::exit(1); + } + transaction_t * xact + = new transaction_t(&added, matching->transactions.back()->account); + added.add_transaction(xact); + } + } + + // if (added.finalize()) + print_textual_entry(std::cout, &added); + + return true; +} + + +void set_price_conversion(const std::string& setting, ledger_t * ledger) +{ + 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'; + + amount_t price; + price.parse(p, ledger); + + commodity_t * commodity = ledger->find_commodity(c, true); + commodity->set_conversion(price); + } +} + + +static long pricing_leeway = 24 * 3600; +static std::string price_db; +static ledger_t * current_ledger = NULL; +static bool cache_dirty = false; + +void download_price_quote(commodity_t * commodity, + const std::time_t age, + const amount_t& price, + const std::time_t moment) +{ + std::time_t now = std::time(NULL); // the time of the query + + if (! (commodity->flags & COMMODITY_STYLE_CONSULTED) && + std::difftime(now, moment) < pricing_leeway && + (! price || std::difftime(moment, age) > pricing_leeway)) { + using namespace std; + + // Only consult the Internet once for any commodity + commodity->flags |= COMMODITY_STYLE_CONSULTED; + cache_dirty = true; + + char buf[256]; + buf[0] = '\0'; + + if (FILE * fp = popen((string("getquote ") + + commodity->symbol).c_str(), "r")) { + if (feof(fp) || ! fgets(buf, 255, fp)) { + fclose(fp); + return; + } + fclose(fp); + } + + if (buf[0]) { + char * p = strchr(buf, '\n'); + if (p) *p = '\0'; + + amount_t current; + current.parse(buf, current_ledger); + + commodity->add_price(now, current); + + if (! price_db.empty()) { + char buf[128]; + strftime(buf, 127, "%Y/%m/%d %H:%M:%S", localtime(&now)); + ofstream database(price_db.c_str(), ios_base::out | ios_base::app); + database << "P " << buf << " " << commodity->symbol << " " + << current << endl; + } + } + } +} + +} // namespace ledger + + +static void show_help(std::ostream& out) +{ + std::cerr + << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl + << std::endl + << "Basic options:" << std::endl + << " -h display this help text" << std::endl + << " -v display version information" << std::endl + << " -f FILE specify pathname of ledger data file" << std::endl + << std::endl + << "Report filtering:" << std::endl + << " -a REGEX specify an account regex for \"print\"" << std::endl + << " -b DATE specify a beginning date" << std::endl + << " -e DATE specify an ending date" << std::endl + << " -c do not show future entries (same as -e TODAY)" << std::endl + << " -d DATE specify a date mask ('-d mon', for all mondays)" << std::endl + << " -C show only cleared transactions and balances" << std::endl + << " -U show only uncleared transactions and balances" << std::endl + << " -R do not consider virtual transactions: real only" << std::endl + << " -l EXPR don't print entries for which EXPR yields 0" << std::endl + << std::endl + << "Customizing output:" << std::endl + << " -n do not calculate parent account totals" << std::endl + << " -s show sub-accounts in balance, and splits in register" << std::endl + << " -M print register using monthly sub-totals" << std::endl + << " -E show accounts that total to zero" << std::endl + << " -S EXPR sort entry output based on EXPR" << std::endl + << std::endl + << "Commodity prices:" << std::endl + << " -T report commodity totals, not their market value" << std::endl + << " -B report cost basis of commodities" << std::endl + << " -V report the market value of commodities" << std::endl + << " -P FILE sets the price database, for reading/writing price info" << std::endl + << " -Q download new price information (when needed) from the Internet" << std::endl + << " (works by running \"getquote SYMBOL\")" << std::endl + << " -L MINS with -Q, fetch quotes only if data is older than MINS" << std::endl + << " -p STR specifies a direct commodity conversion: COMM=AMOUNT" << std::endl + << std::endl + << "commands:" << std::endl + << " balance show balance totals" << std::endl + << " register display a register for ACCOUNT" << std::endl + << " print print all ledger entries" << std::endl + << " equity generate equity ledger for all entries" << std::endl + << " entry output a newly formed entry, based on arguments" << std::endl + << " price show the last known price for matching commodities" << std::endl; +} + +int main(int argc, char * argv[]) +{ + std::list<std::string> files; + ledger::ledger_t * book = NULL; + ledger::constraints_t constraints; + ledger::report::format_t format; + + std::string sort_order; + std::string value_style = "a"; + std::string total_style = "T"; + + // Initialize some variables based on environment variable settings + + if (char * p = std::getenv("PRICE_HIST")) + ledger::price_db = p; + + if (char * p = std::getenv("PRICE_EXP")) + ledger::pricing_leeway = std::atol(p) * 60; + + // A ledger data file must be specified + + bool use_cache = std::getenv("LEDGER") != NULL; + + if (use_cache) { + for (int i = 0; i < argc; i++) + if (std::strcmp(argv[i], "-f") == 0) { + use_cache = false; + break; + } + + if (use_cache) + if (const char * p = std::getenv("LEDGER_CACHE")) + if (access(p, R_OK) != -1) { + std::ifstream instr(p); + if (! ledger::read_binary_ledger(instr, std::getenv("LEDGER"), + book)) { + delete book; + book = NULL; + } + } + } + + if (! book) { + book = new ledger::ledger_t; + ledger::cache_dirty = true; + } + + ledger::current_ledger = book; + + // Parse the command-line options + + int c, index; + while (-1 != + (c = getopt(argc, argv, + "+a:ABb:Ccd:DEe:F:f:Ghi:L:l:MN:noOP:p:QRS:st:T:UVvWXZ"))) { + switch (char(c)) { + // Basic options + case 'h': + show_help(std::cout); + break; + + case 'v': + std::cout + << "Ledger " << ledger::version + << ", the command-line accounting tool" << std::endl + << " Copyright (c) 2003-2004, New Artisans LLC. All rights reserved." + << std::endl << std::endl + << "This program is made available under the terms of the BSD Public" + << std::endl + << "License. See the LICENSE file included with the distribution for" + << std::endl + << "details and disclaimer." << std::endl; + return 0; + + case 'f': + files.push_back(optarg); + use_cache = false; + break; + + case 'p': + ledger::set_price_conversion(optarg, book); + break; + + // Constraint options + case 'a': + constraints.account_masks.push_back(ledger::mask_t(optarg)); + break; + + case 'b': + constraints.have_beginning = true; + if (! ledger::parse_date(optarg, &constraints.begin_date)) { + std::cerr << "Error: Bad begin date: " << optarg << std::endl; + return 1; + } + break; + + case 'e': + constraints.have_ending = true; + if (! ledger::parse_date(optarg, &constraints.end_date)) { + std::cerr << "Error: Bad end date: " << optarg << std::endl; + return 1; + } + break; + + case 'c': + constraints.end_date = std::time(NULL); + constraints.have_ending = true; + break; + + case 'd': + constraints.have_date_mask = true; + if (! ledger::parse_date_mask(optarg, &constraints.date_mask)) { + std::cerr << "Error: Bad date mask: " << optarg << std::endl; + return 1; + } + break; + + case 'C': + constraints.cleared_only = true; + break; + + case 'U': + constraints.uncleared_only = true; + break; + + case 'R': + constraints.real_only = true; + break; + + // Customizing output + case 'F': + format.format_string = optarg; + break; + + case 'M': + format.period = ledger::report::PERIOD_MONTHLY; + break; + + case 'E': + format.show_empty = true; + break; + + case 'n': + format.show_subtotals = false; + break; + + case 's': + format.show_expanded = true; + break; + + case 'S': + sort_order = optarg; + break; + + case 'o': + format.show_related = true; + break; + + case 'l': + format.predicate = ledger::report::parse_expr(optarg, book); + break; + + // Commodity reporting + case 'P': + ledger::price_db = optarg; + break; + + case 'L': + ledger::pricing_leeway = std::atol(optarg) * 60; + break; + + case 'Q': + ledger::commodity_t::updater = ledger::download_price_quote; + break; + + case 't': + value_style = optarg; + break; + + case 'T': + total_style = optarg; + break; + + case 'O': + value_style = "a"; + total_style = "T"; + break; + + case 'B': + value_style = "c"; + total_style = "C"; + break; + + case 'V': + ledger::show_commodities_revalued = true; + + value_style = "v"; + total_style = "V"; + break; + + case 'G': + ledger::show_commodities_revalued = + ledger::show_commodities_revalued_only = true; + + value_style = "c"; + total_style = "G"; + break; + + case 'A': + value_style = "a"; + total_style = "MT"; + break; + + case 'D': + value_style = "a"; + total_style = "DMT"; + break; + + case 'Z': + value_style = "a"; + total_style = "MDMT"; + break; + + case 'W': + value_style = "a"; + total_style = "MD(MT*(d-b/e-b))"; + break; + + case 'X': + value_style = "a"; + total_style = "a+MD(MT*(d-b/e-b))"; + break; + } + } + + if (optind == argc) { + show_help(std::cout); + return 1; + } + + index = optind; + + // Read the ledger file, unless we already read it from the cache + + if (! use_cache || ledger::cache_dirty) { + int entry_count = 0; + + if (files.empty()) { + if (char * p = std::getenv("LEDGER")) + for (p = std::strtok(p, ":"); p; p = std::strtok(NULL, ":")) + entry_count += parse_ledger_file(p, book); + } else { + for (std::list<std::string>::iterator i = files.begin(); + i != files.end(); i++) { + char buf[4096]; + char * p = buf; + std::strcpy(p, (*i).c_str()); + entry_count += parse_ledger_file(p, book); + } + } + + // Read prices from their own ledger file, after all others have + // been read. + + if (! ledger::price_db.empty()) { + const char * path = ledger::price_db.c_str(); + std::ifstream db(path); + book->sources.push_back(path); + entry_count += ledger::parse_textual_ledger(db, book, book->master); + } + + if (entry_count == 0) { + std::cerr << ("Please specify ledger file(s) using -f option " + "or LEDGER environment variable.") << std::endl; + return 1; + } + } + + // Read the command word, and handle the "entry" command specially, + // without any other processing. + + const std::string command = argv[index++]; + + if (command == "entry") + return add_new_entry(index, argc, argv, book) ? 0 : 1; + + // Interpret the remaining arguments as regular expressions, used + // for refining report results. + + for (; index < argc; index++) { + if (std::strcmp(argv[index], "--") == 0) { + index++; + break; + } + constraints.account_masks.push_back(ledger::mask_t(argv[index])); + } + + for (; index < argc; index++) + constraints.payee_masks.push_back(ledger::mask_t(argv[index])); + + // Copy the constraints to the format object, and compile the value + // and total style strings + + format.constraints = constraints; + if (! sort_order.empty()) + format.sort_order = ledger::report::parse_expr(sort_order, book); + format.value_style = ledger::report::parse_expr(value_style, book); + format.total_style = ledger::report::parse_expr(total_style, book); + + // Now handle the command that was identified above. + + if (command == "print") { +#if 0 + ledger::report::item_t * top + = ledger::report::walk_entries(book->entries.begin(), + book->entries.end(), + constraints, format); + ledger::entry_report(std::cout, top, format); +#ifdef DEBUG + delete top; +#endif +#endif + } + else if (command == "equity") { +#if 0 + ledger::report::item_t * top + = ledger::report::walk_accounts(book->master, constraints, + format.show_subtotals); + + ledger::entry_report(std::cout, top, constraints, format); + +#ifdef DEBUG + delete top; +#endif +#endif + } + else if (format.period == ledger::report::PERIOD_NONE && + ! format.sort_order && ! format.show_related && + (command == "balance" || command == "bal")) { + if (format.format_string.empty()) + format.format_string = ledger::bal_fmt; + + if (ledger::report::item_t * top + = ledger::report::walk_accounts(book->master, constraints, + format.show_subtotals)) { + ledger::balance_report(std::cout, top, constraints, format); +#ifdef DEBUG + delete top; +#endif + } + } + else if (command == "balance" || command == "bal") { + if (format.format_string.empty()) + format.format_string = ledger::bal_fmt; + + if (ledger::report::item_t * list + = ledger::report::walk_entries(book->entries.begin(), + book->entries.end(), + constraints, format)) + if (ledger::report::item_t * top + = ledger::report::walk_items(list, book->master, constraints, + format.show_subtotals)) { + ledger::balance_report(std::cout, top, constraints, format); +#ifdef DEBUG + delete top; + delete list; +#endif + } + } + else if (command == "register" || command == "reg") { + if (format.format_string.empty()) + format.format_string = ledger::reg_fmt; + + if (format.show_related) + format.show_inverted = true; + + if (ledger::report::item_t * top + = ledger::report::walk_entries(book->entries.begin(), + book->entries.end(), + constraints, format)) { + ledger::register_report(std::cout, top, format); +#ifdef DEBUG + delete top; +#endif + } + } + else { + std::cerr << "Error: Unrecognized command '" << command << "'." + << std::endl; + return 1; + } + + // Save the cache, if need be + + if (use_cache && ledger::cache_dirty) + if (const char * p = std::getenv("LEDGER_CACHE")) { + std::ofstream outstr(p); + assert(std::getenv("LEDGER")); + ledger::write_binary_ledger(outstr, book, std::getenv("LEDGER")); + } + +#ifdef DEBUG + delete book; +#endif + + return 0; +} + +// main.cc ends here. diff --git a/parse.cc b/parse.cc deleted file mode 100644 index e4850973..00000000 --- a/parse.cc +++ /dev/null @@ -1,582 +0,0 @@ -#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 diff --git a/reports.cc b/reports.cc deleted file mode 100644 index 2708c1d0..00000000 --- a/reports.cc +++ /dev/null @@ -1,1225 +0,0 @@ -#include "ledger.h" - -#define LEDGER_VERSION "1.7" - -#include <cstring> -#include <unistd.h> - -namespace ledger { - -static bool cleared_only = false; -static bool uncleared_only = false; -static bool show_virtual = true; -static bool show_children = false; -static bool show_sorted = false; -static bool show_empty = false; -static bool show_subtotals = true; -static bool full_names = false; -static bool print_monthly = false; -static bool gnuplot_safe = false; - -static bool cost_basis = false; -static bool use_history = false; -static bool net_gain = false; -static bool get_quotes = false; - long pricing_leeway = 24 * 3600; - std::string price_db; - -static amount * lower_limit = NULL; - -static mask * negonly_regexp = NULL; - -static std::time_t begin_date; -static bool have_beginning = false; - -static std::time_t end_date; -static bool have_ending = false; - -static struct std::tm date_mask; -static bool have_date_mask = false; - -static bool matches_date_range(entry * ent) -{ - if (have_beginning && difftime(ent->date, begin_date) < 0) - return false; - - if (have_ending && difftime(ent->date, end_date) >= 0) - return false; - - if (have_date_mask) { - struct std::tm * then = std::localtime(&ent->date); - - if (date_mask.tm_mon != -1 && - date_mask.tm_mon != then->tm_mon) - return false; - - if (date_mask.tm_mday != -1 && - date_mask.tm_mday != then->tm_mday) - return false; - -#if 0 - // jww (2003-10-10): This causes only certain days of the week to - // print, even when it was not included in the mask. - if (date_mask.tm_wday != -1 && - date_mask.tm_wday != then->tm_wday) - return false; -#endif - - if (date_mask.tm_year != -1 && - date_mask.tm_year != then->tm_year) - return false; - } - - return true; -} - -static amount * resolve_amount(amount * amt, - std::time_t * when = NULL, - totals * balance = NULL, - bool add_base_value = false, - bool free_memory = false) -{ - amount * value; - bool alloced = true; - - if (! use_history) { - value = amt; - alloced = false; - } - else if (cost_basis) { - value = amt->value(); - } - else if (net_gain) { - value = amt->street(when ? when : (have_ending ? &end_date : NULL), - use_history, get_quotes); - amount * basis = amt->value(); - if (value->commdty() == basis->commdty()) { - basis->negate(); - value->credit(basis); - } else { - // If the commodities do not match, ignore this amount by - // returning a zeroed value. - delete basis; - basis = value->copy(); - basis->negate(); - value->credit(basis); - delete basis; - } - } - else { - value = amt->street(when ? when : (have_ending ? &end_date : NULL), - use_history, get_quotes); - } - - if (balance) { - if (add_base_value) - balance->credit(cost_basis ? value : amt); - else - balance->credit(value); - } - - if (free_memory && alloced) { - delete value; - value = NULL; - } - else if (! free_memory && ! alloced) { - value = value->copy(); - } - - return value; -} - -static inline void print_resolved_balance(std::ostream& out, - std::time_t * when, - totals& balance, - bool added_base_value = false) -{ - if (! added_base_value || ! use_history || cost_basis) { - balance.print(out, 12); - } else { - totals * street = balance.street(when ? when : (have_ending ? - &end_date : NULL), - use_history, get_quotes); - street->print(out, 12); - delete street; - } -} - -////////////////////////////////////////////////////////////////////// -// -// Balance reporting code -// - -static bool satisfies_limit(totals& balance) -{ - bool satisfies = true; - bool invert = false; - - assert(lower_limit); - - if (balance.is_negative()) - invert = true; - else - lower_limit->negate(); - - balance.credit(lower_limit); - if (balance.is_negative()) - satisfies = invert; - else - satisfies = ! invert; - - lower_limit->negate(); - balance.credit(lower_limit); - - if (invert) - lower_limit->negate(); - - return satisfies; -} - -static bool satisfies_limit(amount * balance) -{ - bool satisfies = true; - bool invert = false; - - assert(lower_limit); - - if (balance->is_negative()) - invert = true; - else - lower_limit->negate(); - - balance->credit(lower_limit); - if (balance->is_negative()) - satisfies = invert; - else - satisfies = ! invert; - - lower_limit->negate(); - balance->credit(lower_limit); - - if (invert) - lower_limit->negate(); - - return satisfies; -} - -static void adjust_total(account * acct) -{ - for (accounts_map_iterator i = acct->children.begin(); - i != acct->children.end(); - i++) - adjust_total((*i).second); - - if (acct->checked == 1) { - if (! show_empty && acct->balance.is_zero()) - acct->checked = 2; - else if (lower_limit && ! satisfies_limit(acct->balance)) - acct->checked = 2; - else if (negonly_regexp && negonly_regexp->match(acct->as_str()) && - ! acct->balance.is_negative()) - acct->checked = 2; - - if (acct->checked == 2) { - acct->balance.negate(); - for (account * a = acct->parent; a; a = a->parent) - a->balance.credit(acct->balance); - } - } -} - -static int acct_visible_children(account * acct) -{ - int count = 0; - for (accounts_map_iterator i = acct->children.begin(); - i != acct->children.end(); - i++) { - if ((*i).second->checked == 1) { - if ((*i).second->children.size() == 0) - count++; - else - count += acct_visible_children((*i).second); - } - } - return count; -} - -static void display_total(std::ostream& out, totals& balance, - account * acct, int level, int * headlines) -{ - // If the number of visible children is exactly one, do not print - // the parent account, but just the one child (whose name will - // output with sufficiently qualification). - - if (acct->checked == 1 && acct_visible_children(acct) != 1) { - if (acct->balance.is_zero()) { - out.width(20); - out << " "; - } else { - acct->balance.print(out, 20); - } - - if (level == 0 || full_names || ! show_subtotals) { - if (show_subtotals) { - balance.credit(acct->balance); - (*headlines)++; - } - - out << " " << acct->as_str() << std::endl; - } else { - out << " "; - for (int i = 0; i < level; i++) - out << " "; - - assert(acct->parent); - if (acct_visible_children(acct->parent) == 1) { - /* If the account has no other siblings, instead of printing: - Parent - Child - print: - Parent:Child */ - const account * parent; - for (parent = acct->parent; - parent->parent && acct_visible_children(parent->parent) == 1; - parent = parent->parent) {} - - out << acct->as_str(parent) << std::endl; - } else { - out << acct->name << std::endl; - } - } - - level++; - } - - // Display balances for all child accounts - - for (accounts_map_iterator i = acct->children.begin(); - i != acct->children.end(); - i++) - display_total(out, balance, (*i).second, level, headlines); -} - -void report_balances(std::ostream& out, regexps_list& regexps) -{ - // Walk through all of the ledger entries, computing the account - // totals - - for (entries_list_iterator i = main_ledger->entries.begin(); - i != main_ledger->entries.end(); - i++) { - if ((cleared_only && ! (*i)->cleared) || - (uncleared_only && (*i)->cleared) || ! matches_date_range(*i)) - continue; - - for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); - x != (*i)->xacts.end(); - x++) { - if (! show_virtual && (*x)->is_virtual) - continue; - - for (account * acct = (*x)->acct; - acct; - acct = show_subtotals ? acct->parent : NULL) { - bool by_exclusion = false; - bool match = false; - - if (acct->checked == 0) { - if (regexps.empty()) { - if (! (show_children || ! acct->parent)) - acct->checked = 2; - else - acct->checked = 1; - } else { - match = matches(regexps, acct->as_str(), &by_exclusion); - if (! match) { - acct->checked = 2; - } - else if (by_exclusion) { - if (! (show_children || ! acct->parent)) - acct->checked = 2; - else - acct->checked = 1; - } - else { - acct->checked = 1; - } - } - } - - if (acct->checked == 1) { - resolve_amount((*x)->cost, NULL, &acct->balance, false, true); - } - else if (show_subtotals) { - if (! regexps.empty() && ! match) { - for (account * a = acct->parent; a; a = a->parent) { - if (matches(regexps, a->as_str(), &by_exclusion) && - ! by_exclusion) { - match = true; - break; - } - } - if (! match) break; - } - } - } - } - } - - // Walk through all the top-level accounts, giving the balance - // report for each, and then for each of their children. - - totals balance; - int headlines = 0; - - for (accounts_map_iterator i = main_ledger->accounts.begin(); - i != main_ledger->accounts.end(); - i++) { - adjust_total((*i).second); - display_total(out, balance, (*i).second, 0, &headlines); - } - - // Print the total of all the balances shown - - if (show_subtotals && headlines > 1 && ! balance.is_zero()) { - out << "--------------------" << std::endl; - balance.print(out, 20); - out << std::endl; - } -} - -////////////////////////////////////////////////////////////////////// -// -// Register printing code -// - -static std::string truncated(const std::string& str, int width) -{ - char buf[256]; - memset(buf, '\0', 255); - std::strncpy(buf, str.c_str(), width); - if (buf[width - 1]) - std::strcpy(&buf[width - 3], "..."); - else - buf[width] = '\0'; - return buf; -} - -enum periodicity_t { - PERIOD_NONE, - PERIOD_MONTHLY, - PERIOD_WEEKLY_SUN, - PERIOD_WEEKLY_MON -}; - -static totals * prev_balance = NULL; -static std::time_t prev_date; - -void print_register_transaction(std::ostream& out, entry * ent, - transaction * xact, totals& balance); - -static void report_change_in_asset_value(std::ostream& out, std::time_t date, - account * acct, totals& balance) -{ - totals * prev_street_balance = - prev_balance->street(&prev_date, use_history, get_quotes); - totals * curr_street_balance = - prev_balance->street(&date, use_history, get_quotes); - - delete prev_balance; - prev_balance = NULL; - - prev_street_balance->negate(); - curr_street_balance->credit(*prev_street_balance); - - if (! curr_street_balance->is_zero()) { - for (totals::const_iterator i = curr_street_balance->amounts.begin(); - i != curr_street_balance->amounts.end(); - i++) { - if ((*i).second->is_zero()) - continue; - - entry change(main_ledger); - - change.date = date; - change.cleared = true; - change.desc = "Assets revalued"; - - transaction * trans = new transaction(); - trans->acct = const_cast<account *>(acct); - trans->cost = (*i).second->copy(); - change.xacts.push_back(trans); - - transaction * trans2 = new transaction(); - trans2->acct = main_ledger->find_account("Equity:Asset Gain"); - trans2->cost = (*i).second->copy(); - trans2->cost->negate(); - change.xacts.push_back(trans2); - - balance.credit(trans2->cost); - - print_register_transaction(out, &change, trans, balance); - - delete prev_balance; - prev_balance = NULL; - } - } - - delete prev_street_balance; - delete curr_street_balance; -} - -void print_register_transaction(std::ostream& out, entry * ent, - transaction * xact, totals& balance) -{ - if (prev_balance) - report_change_in_asset_value(out, ent->date, xact->acct, balance); - - char buf[32]; - std::strftime(buf, 31, "%Y/%m/%d ", std::localtime(&ent->date)); - out << buf; - - out.width(20); - if (ent->desc.empty()) - out << " "; - else - out << std::left << truncated(ent->desc, 20); - out << " "; - - // Always display the street value, if prices have been - // specified - - amount * street = resolve_amount(xact->cost, &ent->date, &balance, true); - - // If there are two transactions, use the one which does not - // refer to this account. If there are more than two, print - // "<Splits...>", unless the -s option is being used (show - // children), in which case print all of the splits, like - // gnucash does. - - transaction * xp; - if (ent->xacts.size() == 2) { - if (xact == ent->xacts.front()) - xp = ent->xacts.back(); - else - xp = ent->xacts.front(); - } else { - xp = xact; - } - std::string xact_str = xp->acct_as_str(); - - if (xp == xact && ! show_subtotals) - xact_str = "<Splits...>"; - - out.width(22); - out << std::left << truncated(xact_str, 22) << " "; - - out.width(12); - out << std::right << street->as_str(true); - delete street; - - print_resolved_balance(out, &ent->date, balance, true); - - out << std::endl; - - assert(! prev_balance); - prev_balance = new totals; - prev_balance->credit(balance); - prev_date = ent->date; - - if (! show_children || xp != xact) - return; - - for (std::list<transaction *>::iterator y = ent->xacts.begin(); - y != ent->xacts.end(); - y++) { - if (xact == *y) - continue; - - out << " "; - - out.width(22); - out << std::left << truncated((*y)->acct_as_str(), 22) << " "; - out.width(12); - - street = resolve_amount((*y)->cost, &ent->date); - out << std::right << street->as_str(true) << std::endl; - delete street; - } -} - -void print_register_period(std::ostream& out, std::time_t date, - account * acct, amount& sum, totals& balance) -{ - if (! gnuplot_safe && prev_balance) { - sum.negate(); - balance.credit(&sum); - report_change_in_asset_value(out, date, acct, balance); - sum.negate(); - balance.credit(&sum); - } - - char buf[32]; - std::strftime(buf, 31, "%Y/%m/%d ", std::localtime(&date)); - out << buf; - - if (! gnuplot_safe) { - out.width(20); - std::strftime(buf, 31, "%B", std::localtime(&date)); - out << std::left << truncated(buf, 20); - out << " "; - - out.width(22); - out << std::left << truncated(acct->as_str(), 22) << " "; - } else { - commodity * cmdty = sum.commdty(); - cmdty->symbol = ""; - cmdty->separate = false; - cmdty->thousands = false; - cmdty->european = false; - } - - out.width(12); - out << std::right << sum.as_str(); - - if (! gnuplot_safe) { - print_resolved_balance(out, &date, balance, true); - - assert(! prev_balance); - prev_balance = new totals; - prev_balance->credit(balance); - prev_date = date; - } - - out << std::endl; -} - -void print_register(std::ostream& out, const std::string& acct_name, - regexps_list& regexps, periodicity_t period = PERIOD_NONE) -{ - mask acct_regex(acct_name); - - // Walk through all of the ledger entries, printing their register - // formatted equivalent - - totals balance; - amount * period_sum = NULL; // jww (2004-04-27): should be 'totals' type - std::time_t last_date; - account * last_acct; - int last_mon = -1; - - for (entries_list_iterator i = main_ledger->entries.begin(); - i != main_ledger->entries.end(); - i++) { - if ((cleared_only && ! (*i)->cleared) || - (uncleared_only && (*i)->cleared) || - ! matches_date_range(*i) || ! (*i)->matches(regexps)) - continue; - - int entry_mon = std::localtime(&(*i)->date)->tm_mon; - - if (period_sum && period == PERIOD_MONTHLY && - last_mon != -1 && entry_mon != last_mon) { - assert(last_acct); - print_register_period(out, last_date, last_acct, *period_sum, balance); - delete period_sum; - period_sum = NULL; - } - - for (std::list<transaction *>::iterator x = (*i)->xacts.begin(); - x != (*i)->xacts.end(); - x++) { - if (! acct_regex.match((*x)->acct->as_str()) || - (lower_limit && ! satisfies_limit((*x)->cost))) - continue; - - if (period == PERIOD_NONE) { - print_register_transaction(out, *i, *x, balance); - } else { - amount * street = resolve_amount((*x)->cost, &(*i)->date, &balance, - true); - if (period_sum) { - period_sum->credit(street); - delete street; - } else { - period_sum = street; - } - - last_mon = entry_mon; - } - - last_date = (*i)->date; - last_acct = (*x)->acct; - } - } - - if (period_sum) { - if (last_acct) - print_register_period(out, last_date, last_acct, *period_sum, balance); - delete period_sum; - } - - if (! gnuplot_safe && prev_balance) { - report_change_in_asset_value(out, have_ending ? end_date : std::time(NULL), - last_acct, balance); - } -} - -////////////////////////////////////////////////////////////////////// -// -// Create an Equity file based on a ledger. This is used for -// archiving past years, and starting out a new year with compiled -// balances. -// - -static void equity_entry(account * acct, regexps_list& regexps, - std::ostream& out) -{ - if (! acct->balance.is_zero() && - (regexps.empty() || matches(regexps, acct->as_str()))) { - entry opening(main_ledger); - - opening.date = have_ending ? end_date : std::time(NULL); - opening.cleared = true; - opening.desc = "Opening Balance"; - - for (totals::const_iterator i = acct->balance.amounts.begin(); - i != acct->balance.amounts.end(); - i++) { - // Skip it, if there is a zero balance for the commodity - if ((*i).second->is_zero()) - continue; - - transaction * xact = new transaction(); - xact->acct = const_cast<account *>(acct); - xact->cost = (*i).second->copy(); - opening.xacts.push_back(xact); - - xact = new transaction(); - xact->acct = main_ledger->find_account("Equity:Opening Balances"); - xact->cost = (*i).second->copy(); - xact->cost->negate(); - opening.xacts.push_back(xact); - } - - opening.print(out); - } - - // Display balances for all child accounts - - for (accounts_map_iterator i = acct->children.begin(); - i != acct->children.end(); - i++) - equity_entry((*i).second, regexps, out); -} - -void equity_ledger(std::ostream& out, regexps_list& regexps) -{ - // The account have their current totals already generated as a - // result of parsing. We just have to output those values. - // totals - - for (accounts_map_iterator i = main_ledger->accounts.begin(); - i != main_ledger->accounts.end(); - i++) - equity_entry((*i).second, regexps, out); -} - -////////////////////////////////////////////////////////////////////// -// -// Report on the price of any commodities matching REGEXPS. This can -// be used to see what something was worth at a specific time. -// - -void price_report(std::ostream& out, regexps_list& regexps) -{ - if (! have_ending) { - end_date = std::time(NULL); - have_ending = true; - } - - for (commodities_map_iterator i = main_ledger->commodities.begin(); - i != main_ledger->commodities.end(); - i++) - if (regexps.empty() || matches(regexps, (*i).first)) { - amount * price = (*i).second->price(have_ending ? &end_date : NULL, - use_history, get_quotes); - if (price && ! price->is_zero()) { - out.width(20); - out << std::right << price->as_str() << " " << (*i).first - << std::endl; - } - } -} - -////////////////////////////////////////////////////////////////////// -// -// Add a new entry, using hueristic logic to simplify the entry -// requirements -// - -void add_new_entry(int index, int argc, char **argv) -{ - regexps_list regexps; - entry added(main_ledger); - entry * matching = NULL; - - assert(index < argc); - - if (! parse_date(argv[index++], &added.date)) { - std::cerr << "Error: Bad entry date: " << argv[index - 1] - << std::endl; - std::exit(1); - } - - added.cleared = cleared_only; - - if (index == argc) { - std::cerr << "Error: Too few arguments to 'entry'." << std::endl; - std::exit(1); - } - - regexps.clear(); - regexps.push_back(mask(argv[index++])); - - for (entries_list_reverse_iterator i = main_ledger->entries.rbegin(); - i != main_ledger->entries.rend(); - i++) { - if ((*i)->matches(regexps)) { - matching = *i; - break; - } - } - - added.desc = matching ? matching->desc : regexps.front().pattern; - - if (index == argc) { - std::cerr << "Error: Too few arguments to 'entry'." << std::endl; - std::exit(1); - } - - if (argv[index][0] == '-' || std::isdigit(argv[index][0])) { - if (! matching) { - std::cerr << "Error: Missing account name for non-matching entry." - << std::endl; - std::exit(1); - } - - transaction * m_xact, * xact, * first; - - m_xact = matching->xacts.front(); - - first = xact = new transaction(); - xact->acct = m_xact->acct; - xact->cost = create_amount(argv[index++]); - xact->cost->set_commdty(m_xact->cost->commdty()); - - added.xacts.push_back(xact); - - m_xact = matching->xacts.back(); - - xact = new transaction(); - xact->acct = m_xact->acct; - xact->cost = first->cost->copy(); - xact->cost->negate(); - - added.xacts.push_back(xact); - - if ((index + 1) < argc && std::string(argv[index]) == "-from") - if (account * acct = main_ledger->re_find_account(argv[++index])) - added.xacts.back()->acct = acct; - } else { - while (index < argc && std::string(argv[index]) != "-from") { - transaction * xact = new transaction(); - - mask acct_regex(argv[index++]); - - account * acct = NULL; - commodity * cmdty = NULL; - - if (matching) { - for (std::list<transaction *>::iterator x = matching->xacts.begin(); - x != matching->xacts.end(); - x++) { - if (acct_regex.match((*x)->acct->as_str())) { - acct = (*x)->acct; - cmdty = (*x)->cost->commdty(); - break; - } - } - } - - if (acct) - xact->acct = acct; - else - xact->acct = main_ledger->re_find_account(acct_regex.pattern); - - if (! xact->acct) { - std::cerr << "Error: Could not find account name '" - << acct_regex.pattern << "'." << std::endl; - std::exit(1); - } - - if (index == argc) { - std::cerr << "Error: Too few arguments to 'entry'." << std::endl; - std::exit(1); - } - - xact->cost = create_amount(argv[index++]); - if (! xact->cost->commdty()) - xact->cost->set_commdty(cmdty); - - added.xacts.push_back(xact); - } - - if ((index + 1) < argc && std::string(argv[index]) == "-from") { - if (account * acct = main_ledger->re_find_account(argv[++index])) { - transaction * xact = new transaction(); - xact->acct = acct; - xact->cost = NULL; - - added.xacts.push_back(xact); - } - } else { - transaction * xact = new transaction(); - if (! matching) { - std::cerr << "Error: Could not figure out the account to draw from." - << std::endl; - std::exit(1); - } - xact->acct = matching->xacts.back()->acct; - xact->cost = NULL; - added.xacts.push_back(xact); - } - } - - if (added.finalize()) - added.print(std::cout); -} - -// Print out the entire ledger that was read in. This can be used to -// "wash" ugly ledger files. It's written here, instead of ledger.cc, -// in order to access the static globals above. - -void book::print(std::ostream& out, regexps_list& regexps, - bool shortcut) const -{ - for (entries_list_const_iterator i = entries.begin(); - i != entries.end(); - i++) { - if (! matches_date_range(*i) || ! (*i)->matches(regexps)) - continue; - - (*i)->print(out, shortcut); - } -} - -} // namespace ledger - -using namespace ledger; - -static void show_help(std::ostream& out) -{ - std::cerr - << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl - << std::endl - << "Basic options:" << std::endl - << " -h display this help text" << std::endl - << " -v display version information" << std::endl - << " -f FILE specify pathname of ledger data file" << std::endl - << " -i FILE read list of inclusion regexps from FILE" << std::endl - << std::endl - << "Report filtering:" << std::endl - << " -b DATE specify a beginning date" << std::endl - << " -e DATE specify an ending date" << std::endl - << " -c do not show future entries (same as -e TODAY)" << std::endl - << " -d DATE specify a date mask ('-d mon', for all mondays)" << std::endl - << " -C show only cleared transactions and balances" << std::endl - << " -U show only uncleared transactions and balances" << std::endl - << " -l AMT don't print balance totals whose abs value is <AMT" << std::endl - << " -N REGEX accounts matching REGEXP only display if negative" << std::endl - << " -R do not consider virtual transactions: real only" << std::endl - << std::endl - << "Customizing output:" << std::endl - << " -n do not calculate parent account totals" << std::endl - << " -s show sub-accounts in balance totals" << std::endl - << " -S sort the output of \"print\" by date" << std::endl - << " -E show accounts that total to zero" << std::endl - << " -F print each account's full name" << std::endl - << " -M print register using monthly sub-totals" << std::endl - << " -G use with -M to produce gnuplot-friendly output" << std::endl - << std::endl - << "Commodity prices:" << std::endl - << " -P FILE sets the price database, for reading/writing price info" << std::endl - << " -T report commodity totals, not their market value" << std::endl - << " -V report the market value of commodities" << std::endl - << " -B report cost basis of commodities" << std::endl - << " -Q download new price information (when needed) from the Internet" << std::endl - << " (works by running \"getquote SYMBOL\")" << std::endl - << " -L MINS with -Q, fetch quotes only if data is older than MINS" << std::endl - << " -p STR specifies a direct commodity conversion: COMM=AMOUNT" << std::endl - << std::endl - << "commands:" << std::endl - << " balance show balance totals" << std::endl - << " register display a register for ACCOUNT" << std::endl - << " print print all ledger entries" << std::endl - << " equity generate equity ledger for all entries" << std::endl - << " entry output a newly formed entry, based on arguments" << std::endl - << " price show the last known price for matching commodities" << std::endl; -} - -////////////////////////////////////////////////////////////////////// -// -// Command-line parser and top-level logic. -// - -int main(int argc, char * argv[]) -{ - int index; - std::string prices; - std::string limit; - regexps_list regexps; - - std::vector<std::string> files; - - main_ledger = new book; - - // Initialize some variables based on environment variable settings - - if (char * p = std::getenv("PRICE_HIST")) - price_db = p; - - if (char * p = std::getenv("PRICE_EXP")) - pricing_leeway = std::atol(p) * 60; - - // Parse the command-line options - - int c; - while (-1 != (c = getopt(argc, argv, - "+ABb:Ccd:Ee:Ff:Ghi:L:l:MN:nP:p:QRSsTUVv"))) { - switch (char(c)) { - case 'b': - have_beginning = true; - if (! parse_date(optarg, &begin_date)) { - std::cerr << "Error: Bad begin date: " << optarg << std::endl; - return 1; - } - break; - - case 'e': - have_ending = true; - if (! parse_date(optarg, &end_date)) { - std::cerr << "Error: Bad end date: " << optarg << std::endl; - return 1; - } - break; - - case 'c': - end_date = std::time(NULL); - have_ending = true; - break; - - case 'd': - have_date_mask = true; - if (! parse_date_mask(optarg, &date_mask)) { - std::cerr << "Error: Bad date mask: " << optarg << std::endl; - return 1; - } - break; - - case 'h': show_help(std::cout); break; - case 'f': files.push_back(optarg); break; - case 'C': cleared_only = true; break; - case 'U': uncleared_only = true; break; - case 'R': show_virtual = false; break; - case 's': show_children = true; break; - case 'S': show_sorted = true; break; - case 'E': show_empty = true; break; - case 'n': show_subtotals = false; break; - case 'F': full_names = true; break; - case 'M': print_monthly = true; break; - case 'G': gnuplot_safe = true; break; - - case 'N': - negonly_regexp = new mask(optarg); - break; - - // -i path-to-file-of-regexps - case 'i': - if (access(optarg, R_OK) != -1) - read_regexps(optarg, regexps); - break; - - // -p "COMMODITY=PRICE" - case 'p': - parse_price_setting(optarg); - break; - - case 'P': - price_db = optarg; - break; - - case 'Q': - get_quotes = true; - break; - - case 'V': - use_history = true; - break; - - case 'B': - cost_basis = true; - use_history = true; - break; - - case 'A': - net_gain = true; - use_history = true; - break; - - case 'T': - cost_basis = false; - use_history = false; - break; - - case 'L': - pricing_leeway = std::atol(optarg) * 60; - break; - - case 'l': - lower_limit = create_amount(optarg); - break; - - case 'v': - std::cout - << "Ledger Accouting Tool " LEDGER_VERSION << std::endl - << " Copyright (c) 2003 John Wiegley <johnw@newartisans.com>" - << std::endl << std::endl - << "This program is made available under the terms of the BSD" - << std::endl - << "Public License. See the LICENSE file included with the" - << std::endl - << "distribution for details and disclaimer." << std::endl; - return 0; - } - } - - if (optind == argc) { - show_help(std::cout); - return 1; - } - - index = optind; - - // Read the command word - - const std::string command = argv[index++]; - - int name_index = index; - if (command == "register" || command == "reg") { - if (net_gain) { - std::cerr << ("Reporting the asset gain makes " - "no sense for the register report.") - << std::endl; - return 1; - } - - if (name_index == argc) { - std::cerr << ("Error: Must specify an account name " - "after the 'register' command.") << std::endl; - return 1; - } - index++; - } - - // Compile the list of specified regular expressions, which can be - // specified after the command, or using the '-i FILE' option - - if (command != "entry") - for (; index < argc; index++) - regexps.push_back(mask(argv[index])); - - // A ledger data file must be specified - - int entry_count = 0; - - if (files.empty()) { - if (char * p = std::getenv("LEDGER")) { - for (p = std::strtok(p, ":"); p; p = std::strtok(NULL, ":")) { - char * sep = std::strrchr(p, '='); - if (sep) *sep++ = '\0'; - entry_count += parse_ledger_file(main_ledger, std::string(p), regexps, - command == "equity", sep); - } - } - } else { - for (std::vector<std::string>::iterator i = files.begin(); - i != files.end(); i++) { - char buf[4096]; - char * p = buf; - std::strcpy(p, (*i).c_str()); - char * sep = std::strrchr(p, '='); - if (sep) *sep++ = '\0'; - entry_count += parse_ledger_file(main_ledger, std::string(p), regexps, - command == "equity", sep); - } - } - - if (use_history && ! cost_basis && ! price_db.empty()) - entry_count += parse_ledger_file(main_ledger, price_db, regexps, - command == "equity"); - - if (entry_count == 0) { - std::cerr << ("Please specify ledger file(s) using -f option " - "or LEDGER environment variable.") << std::endl; - return 1; - } - - // Process the command - - if (command == "balance" || command == "bal") { - report_balances(std::cout, regexps); - } - else if (command == "register" || command == "reg") { - if (show_sorted || print_monthly) - main_ledger->sort(cmp_entry_date()); - print_register(std::cout, argv[name_index], regexps, - print_monthly ? PERIOD_MONTHLY : PERIOD_NONE); - } - else if (command == "print") { - if (show_sorted) - main_ledger->sort(cmp_entry_date()); - main_ledger->print(std::cout, regexps, ! full_names); - } - else if (command == "equity") { - equity_ledger(std::cout, regexps); - } - else if (command == "price" || command == "prices") { - price_report(std::cout, regexps); - } - else if (command == "entry") { - add_new_entry(index, argc, argv); - } - else { - std::cerr << "Error: Unrecognized command '" << command << "'." - << std::endl; - return 1; - } - -#ifdef DEBUG - // Ordinarily, deleting the main ledger isn't necessary, since the - // process is about to give back its heap to the OS. - - delete main_ledger; - - if (lower_limit) - delete lower_limit; - - if (negonly_regexp) - delete negonly_regexp; -#endif - - return 0; -} - -// reports.cc ends here. diff --git a/sample.dat b/sample.dat deleted file mode 100644 index 4e714275..00000000 --- a/sample.dat +++ /dev/null @@ -1,11 +0,0 @@ -2004/05/01 Checking balance - Assets:Checking $500.00 - Equity:Opening Balances - -2004/05/29 Book Store - Expenses:Books $20.00 - Assets:Checking - -2004/05/29 Restaurant - Expenses:Food $50.00 - Liabilities:MasterCard diff --git a/scripts/README b/scripts/README deleted file mode 100644 index 7221f95f..00000000 --- a/scripts/README +++ /dev/null @@ -1,5 +0,0 @@ -This scripts are provided just in the way of giving ideas. They -probably all need to be modified to suit your particular environment. -Beware! - -John diff --git a/scripts/bal b/scripts/bal deleted file mode 100755 index 7fdf54ca..00000000 --- a/scripts/bal +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -switch="" -current="-c" -limit="-l \$50" -negonly="-N ^Liabilities" - -if [ "$1" = "-C" -o "$1" = "-U" -o "$1" = "-P" ]; then - switch="$1" - shift -elif [ "$1" = "-b" -o "$1" = "-e" ]; then - current="$1 $2" - shift 2 -fi - -accts="$@" -if [ -z "$accts" ]; then - accts="-Equity -Income -Expenses" - if [ ! "$switch" = "-P" ]; then - accts="$accts -Savings -Retirement" - fi -else - limit="" - negonly="" -fi - -ledger $current $limit $negonly -s $switch balance $accts diff --git a/scripts/entry b/scripts/entry deleted file mode 100755 index 28daf8c8..00000000 --- a/scripts/entry +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -if [ -z "$LEDGER" -o ! -r $LEDGER ]; then - echo Please set your LEDGER environment variable. -fi - -line=`wc -l $LEDGER | awk '{print $1}'` - -if ledger entry "$@" > /tmp/entry; then - cat /tmp/entry >> $LEDGER -else - echo "$@" >> $LEDGER -fi -rm /tmp/entry - -vi +$line $LEDGER diff --git a/scripts/getquote b/scripts/getquote deleted file mode 100755 index cf8c8abd..00000000 --- a/scripts/getquote +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/perl - -exit 0 if $ARGV[0] eq "\$"; - -use Finance::Quote; - -$q = Finance::Quote->new; - -$q->timeout(60); -$q->require_labels(qw/price/); - -%quotes = $q->fetch("nasdaq", $ARGV[0]); - -if ($quotes{$ARGV[0], "price"}) { - print "\$", $quotes{$ARGV[0], "price"}, "\n"; -} diff --git a/scripts/mean b/scripts/mean deleted file mode 100755 index 3c6f779a..00000000 --- a/scripts/mean +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/perl - -$last = $ARGV[-1]; -splice(@ARGV, -1); - -open(PIPE, "ledger -MG @ARGV register $last |") || die; -@values = (); -while (<PIPE>) { - ($date, $value) = split; - push @values, $value; -} -close(PIPE); - -@values = sort @values; -splice(@values, 0, 1); -splice(@values, -1); - -$value = 0.0; -for $item (@values) { - $value += $item; -} - -if (@values) { - printf("%.2f\n", $value / @values); -} else { - die "There are no values to average!\n"; -} diff --git a/scripts/profit b/scripts/profit deleted file mode 100755 index 26f112d1..00000000 --- a/scripts/profit +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -ledger "$@" balance ^Income$ ^Expenses$ diff --git a/scripts/reg b/scripts/reg deleted file mode 100755 index 75ac364e..00000000 --- a/scripts/reg +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -switch="-U" -current="-c" - -if [ "$1" = "-C" -o "$1" = "-U" -o "$1" = "-P" -o "$1" = "-M" ]; then - switch="$1" - shift -elif [ "$1" = "-b" -o "$1" = "-e" ]; then - current="$1 $2" - shift 2 -fi - -ledger $current -s $switch register "$@" diff --git a/scripts/report b/scripts/report deleted file mode 100755 index aa42982e..00000000 --- a/scripts/report +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -dir=$HOME/doc/finance - -cd /tmp - -ledger -M -G register "$@" > $1 - -gnuplot <<EOF -set terminal png -set output "report.png" -set xdata time -set timefmt "%Y/%m/%d" -plot "$1" using 1:2 with linespoints -EOF - -open report.png diff --git a/scripts/spending b/scripts/spending deleted file mode 100755 index 895c4170..00000000 --- a/scripts/spending +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -ledger "$@" balance \ - Expenses:Food \ - Expenses:Movies \ - Expenses:Auto:Gas \ - Expenses:Tips \ - Expenses:Health \ - Expenses:Supplies diff --git a/scripts/worth b/scripts/worth deleted file mode 100755 index 58b20440..00000000 --- a/scripts/worth +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -ledger "$@" balance ^Assets$ ^Liabilities$ 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 diff --git a/textual.h b/textual.h new file mode 100644 index 00000000..bda62f02 --- /dev/null +++ b/textual.h @@ -0,0 +1,24 @@ +#ifndef _TEXTUAL_H +#define _TEXTUAL_H + +#include "ledger.h" + +namespace ledger { + +extern unsigned int parse_textual_ledger(std::istream& in, ledger_t *& ledger, + account_t * master = NULL); + +extern bool parse_date_mask(const char * date_str, struct std::tm * result); + +extern bool parse_date(const char * date_str, std::time_t * result, + const int year = -1); + +extern void print_textual_ledger(std::ostream& out, ledger_t * ledger, + bool shortcut = true); + +extern void print_textual_entry(std::ostream& out, entry_t * entry, + bool shortcut = true); + +} // namespace ledger + +#endif // _TEXTUAL_H |